snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alg": "HS256",
|
||||
"k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU",
|
||||
"kid": "8d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8",
|
||||
"kty": "oct",
|
||||
"use": "sig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alg": "HS256",
|
||||
"k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU",
|
||||
"kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8",
|
||||
"kty": "oct",
|
||||
"use": "sig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alg": "HS256",
|
||||
"k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU",
|
||||
"kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8",
|
||||
"kty": "oct",
|
||||
"use": "sig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alg": "HS256",
|
||||
"k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU",
|
||||
"kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8",
|
||||
"kty": "oct",
|
||||
"use": "sig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alg": "HS256",
|
||||
"k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU",
|
||||
"kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8",
|
||||
"kty": "oct",
|
||||
"use": "sig"
|
||||
}
|
||||
74
oryx/jwksx/fetcher.go
Normal file
74
oryx/jwksx/fetcher.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwksx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Fetcher is a small helper for fetching JSON Web Keys from remote endpoints.
|
||||
//
|
||||
// DEPRECATED: Use FetcherNext instead.
|
||||
type Fetcher struct {
|
||||
sync.RWMutex
|
||||
remote string
|
||||
c *http.Client
|
||||
keys map[string]jose.JSONWebKey
|
||||
}
|
||||
|
||||
// NewFetcher returns a new fetcher that can download JSON Web Keys from remote endpoints.
|
||||
//
|
||||
// DEPRECATED: Use FetcherNext instead.
|
||||
func NewFetcher(remote string) *Fetcher {
|
||||
return &Fetcher{
|
||||
remote: remote,
|
||||
c: http.DefaultClient,
|
||||
keys: make(map[string]jose.JSONWebKey),
|
||||
}
|
||||
}
|
||||
|
||||
// GetKey retrieves a JSON Web Key from the cache, fetches it from a remote if it is not yet cached or returns an error.
|
||||
//
|
||||
// DEPRECATED: Use FetcherNext instead.
|
||||
func (f *Fetcher) GetKey(kid string) (*jose.JSONWebKey, error) {
|
||||
f.RLock()
|
||||
if k, ok := f.keys[kid]; ok {
|
||||
f.RUnlock()
|
||||
return &k, nil
|
||||
}
|
||||
f.RUnlock()
|
||||
|
||||
res, err := f.c.Get(f.remote)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.Errorf("expected status code 200 but got %d when requesting %s", res.StatusCode, f.remote)
|
||||
}
|
||||
|
||||
var set jose.JSONWebKeySet
|
||||
if err := json.NewDecoder(res.Body).Decode(&set); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, k := range set.Keys {
|
||||
f.Lock()
|
||||
f.keys[k.KeyID] = k
|
||||
f.Unlock()
|
||||
}
|
||||
|
||||
f.RLock()
|
||||
defer f.RUnlock()
|
||||
if k, ok := f.keys[kid]; ok {
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("unable to find JSON Web Key with ID: %s", kid)
|
||||
}
|
||||
179
oryx/jwksx/fetcher_v2.go
Normal file
179
oryx/jwksx/fetcher_v2.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwksx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"time"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
|
||||
"github.com/ory/x/fetcher"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/ory/x/otelx"
|
||||
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var ErrUnableToFindKeyID = errors.New("specified JWK kid can not be found in the JWK sets")
|
||||
|
||||
type (
|
||||
fetcherNextOptions struct {
|
||||
forceKID string
|
||||
cacheTTL time.Duration
|
||||
useCache bool
|
||||
httpClient *retryablehttp.Client
|
||||
schemes []string
|
||||
}
|
||||
// FetcherNext is a JWK fetcher that can be used to fetch JWKs from multiple locations.
|
||||
FetcherNext struct {
|
||||
cache *ristretto.Cache[[]byte, jwk.Set]
|
||||
}
|
||||
// FetcherNextOption is a functional option for the FetcherNext.
|
||||
FetcherNextOption func(*fetcherNextOptions)
|
||||
)
|
||||
|
||||
// NewFetcherNext returns a new FetcherNext instance.
|
||||
func NewFetcherNext(cache *ristretto.Cache[[]byte, jwk.Set]) *FetcherNext {
|
||||
return &FetcherNext{
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// WithForceKID forces the key ID to be used. Required when multiple JWK sets are configured.
|
||||
func WithForceKID(kid string) FetcherNextOption {
|
||||
return func(o *fetcherNextOptions) {
|
||||
o.forceKID = kid
|
||||
}
|
||||
}
|
||||
|
||||
// WithCacheTTL sets the cache TTL. If not set, the TTL is unlimited.
|
||||
func WithCacheTTL(ttl time.Duration) FetcherNextOption {
|
||||
return func(o *fetcherNextOptions) {
|
||||
o.cacheTTL = ttl
|
||||
}
|
||||
}
|
||||
|
||||
// WithCacheEnabled enables the cache.
|
||||
func WithCacheEnabled() FetcherNextOption {
|
||||
return func(o *fetcherNextOptions) {
|
||||
o.useCache = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithAllowedSchemes(schemes ...string) FetcherNextOption {
|
||||
return func(o *fetcherNextOptions) {
|
||||
o.schemes = schemes
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient will use the given HTTP client to fetch the JSON Web Keys.
|
||||
func WithHTTPClient(c *retryablehttp.Client) FetcherNextOption {
|
||||
return func(o *fetcherNextOptions) {
|
||||
o.httpClient = c
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FetcherNext) ResolveKey(ctx context.Context, locations string, modifiers ...FetcherNextOption) (jwk.Key, error) {
|
||||
return f.ResolveKeyFromLocations(ctx, []string{locations}, modifiers...)
|
||||
}
|
||||
|
||||
func (f *FetcherNext) ResolveKeyFromLocations(ctx context.Context, locations []string, modifiers ...FetcherNextOption) (jwk.Key, error) {
|
||||
opts := new(fetcherNextOptions)
|
||||
for _, m := range modifiers {
|
||||
m(opts)
|
||||
}
|
||||
|
||||
if len(locations) > 1 && opts.forceKID == "" {
|
||||
return nil, errors.Errorf("a key ID must be specified when multiple JWK sets are configured")
|
||||
}
|
||||
|
||||
set := jwk.NewSet()
|
||||
eg := new(errgroup.Group)
|
||||
for k := range locations {
|
||||
location := locations[k]
|
||||
eg.Go(func() error {
|
||||
remoteSet, err := f.fetch(ctx, location, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iterator := remoteSet.Iterate(ctx)
|
||||
for iterator.Next(ctx) {
|
||||
// Pair().Value is always of type jwk.Key when generated by Iterate.
|
||||
set.Add(iterator.Pair().Value.(jwk.Key))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.forceKID != "" {
|
||||
key, found := set.LookupKeyID(opts.forceKID)
|
||||
if !found {
|
||||
return nil, errors.WithStack(ErrUnableToFindKeyID)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// No KID was forced? Use the first key we can find.
|
||||
key, found := set.Get(0)
|
||||
if !found {
|
||||
return nil, errors.WithStack(ErrUnableToFindKeyID)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// fetch fetches the JWK set from the given location and if enabled, may use the cache to look up the JWK set.
|
||||
func (f *FetcherNext) fetch(ctx context.Context, location string, opts *fetcherNextOptions) (_ jwk.Set, err error) {
|
||||
tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("")
|
||||
ctx, span := tracer.Start(ctx, "jwksx.FetcherNext.fetch", trace.WithAttributes(attribute.String("location", location)))
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
cacheKey := sha256.Sum256([]byte(location))
|
||||
if opts.useCache {
|
||||
if result, found := f.cache.Get(cacheKey[:]); found {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
var fopts []fetcher.Modifier
|
||||
if opts.httpClient != nil {
|
||||
fopts = append(fopts, fetcher.WithClient(opts.httpClient))
|
||||
}
|
||||
if len(opts.schemes) > 0 {
|
||||
fopts = append(fopts, fetcher.WithAllowedSchemes(opts.schemes...))
|
||||
}
|
||||
|
||||
result, err := fetcher.NewFetcher(fopts...).FetchContext(ctx, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set, err := jwk.ParseReader(result)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("failed to parse JWK set").WithWrap(err))
|
||||
}
|
||||
|
||||
if opts.useCache {
|
||||
f.cache.SetWithTTL(cacheKey[:], set, 1, opts.cacheTTL)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
129
oryx/jwksx/generator.go
Normal file
129
oryx/jwksx/generator.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwksx
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
// GenerateSigningKeys generates a JSON Web Key Set for signing.
|
||||
func GenerateSigningKeys(id, alg string, bits int) (*jose.JSONWebKeySet, error) {
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
|
||||
key, err := generate(jose.SignatureAlgorithm(alg), bits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{
|
||||
{
|
||||
Algorithm: alg,
|
||||
Use: "sig",
|
||||
Key: key,
|
||||
KeyID: id,
|
||||
Certificates: []*x509.Certificate{},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateSigningKeysAvailableAlgorithms lists available algorithms that are supported by GenerateSigningKeys.
|
||||
func GenerateSigningKeysAvailableAlgorithms() []string {
|
||||
return []string{
|
||||
string(jose.HS256), string(jose.HS384), string(jose.HS512),
|
||||
string(jose.ES256), string(jose.ES384), string(jose.ES512), string(jose.EdDSA),
|
||||
string(jose.RS256), string(jose.RS384), string(jose.RS512), string(jose.PS256), string(jose.PS384), string(jose.PS512),
|
||||
}
|
||||
}
|
||||
|
||||
// generate generates keypair for corresponding SignatureAlgorithm.
|
||||
func generate(alg jose.SignatureAlgorithm, bits int) (crypto.PrivateKey, error) {
|
||||
switch alg {
|
||||
case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA:
|
||||
keylen := map[jose.SignatureAlgorithm]int{
|
||||
jose.ES256: 256,
|
||||
jose.ES384: 384,
|
||||
jose.ES512: 521, // sic!
|
||||
jose.EdDSA: 256,
|
||||
}
|
||||
if bits != 0 && bits != keylen[alg] {
|
||||
return nil, errors.Errorf(`jwksx: "%s" does not support arbitrary key length`, alg)
|
||||
}
|
||||
case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
|
||||
if bits == 0 {
|
||||
bits = 2048
|
||||
}
|
||||
if bits < 2048 {
|
||||
return nil, errors.Errorf(`jwksx: key size must be at least 2048 bit for algorithm "%s"`, alg)
|
||||
}
|
||||
case jose.HS256:
|
||||
if bits == 0 {
|
||||
bits = 256
|
||||
}
|
||||
if bits < 256 {
|
||||
return nil, errors.Errorf(`jwksx: key size must be at least 256 bit for algorithm "%s"`, alg)
|
||||
}
|
||||
case jose.HS384:
|
||||
if bits == 0 {
|
||||
bits = 384
|
||||
}
|
||||
if bits < 384 {
|
||||
return nil, errors.Errorf(`jwksx: key size must be at least 2038448 bit for algorithm "%s"`, alg)
|
||||
}
|
||||
case jose.HS512:
|
||||
if bits == 0 {
|
||||
bits = 1024
|
||||
}
|
||||
if bits < 512 {
|
||||
return nil, errors.Errorf(`jwksx: key size must be at least 512 bit for algorithm "%s"`, alg)
|
||||
}
|
||||
}
|
||||
|
||||
switch alg {
|
||||
case jose.ES256:
|
||||
// The cryptographic operations are implemented using constant-time algorithms.
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
return key, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
case jose.ES384:
|
||||
// NB: The cryptographic operations do not use constant-time algorithms.
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
return key, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
case jose.ES512:
|
||||
// NB: The cryptographic operations do not use constant-time algorithms.
|
||||
key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
return key, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
case jose.EdDSA:
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
return key, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
|
||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
return key, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
case jose.HS256, jose.HS384, jose.HS512:
|
||||
if bits%8 != 0 {
|
||||
return nil, errors.Errorf(`jwksx: key size must be a multiple of 8 for algorithm "%s" but got: %d`, alg, bits)
|
||||
}
|
||||
|
||||
key := make([]byte, bits/8)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, errors.Wrapf(err, "jwks: unable to generate key")
|
||||
}
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.Errorf(`jwksx: available algorithms are "%+v" but unknown algorithm was requested: "%s"`, GenerateSigningKeysAvailableAlgorithms(), alg)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user