Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package clients
import (
"github.com/golang-jwt/jwt/v5"
)
// RegistrationClaims are claims used to with dynamic clients.
type RegistrationClaims struct {
jwt.RegisteredClaims
*ClientRegistration
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package clients
import (
"crypto"
"net/url"
)
// Details hold detail information about clients identified by ID.
type Details struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
RedirectURI string `json:"redirect_uri"`
Trusted bool `json:"trusted"`
Registration *ClientRegistration `json:"-"`
}
// A Secured is a client records public key identified by ID.
type Secured struct {
ID string
DisplayName string
ApplicationType string
Kid string
PublicKey crypto.PublicKey
TrustedScopes []string
Registration *ClientRegistration
}
// IsLocalNativeHTTPURI returns true if the provided URI qualifies to be used
// as http redirect URI for a native client.
func IsLocalNativeHTTPURI(uri *url.URL) bool {
if uri.Scheme != "http" {
return false
}
return IsLocalNativeHostURI(uri)
}
// IsLocalNativeHostURI returns true if the provided URI hostname is considered
// as localhost for a native client.
func IsLocalNativeHostURI(uri *url.URL) bool {
hostname := uri.Hostname()
return hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1"
}
+245
View File
@@ -0,0 +1,245 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package clients
import (
"context"
"crypto"
"crypto/subtle"
"encoding/base64"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/longsleep/rndm"
"github.com/mendsley/gojwk"
"golang.org/x/crypto/blake2b"
_ "gopkg.in/yaml.v2" // Make sure we have yaml.
)
// Constat data used with dynamic stateless clients.
const (
DynamicStatelessClientIDPrefix = "dyn."
DynamicStatelessClientStaticSaltV1 = "konnect-client-v1"
)
// RegistryData is the base structur of our client registry configuration file.
type RegistryData struct {
Clients []*ClientRegistration `yaml:"clients,flow"`
}
// ClientRegistration defines a client with its properties.
type ClientRegistration struct {
ID string `yaml:"id" json:"-"`
Secret string `yaml:"secret" json:"-"`
Trusted bool `yaml:"trusted" json:"-"`
TrustedScopes []string `yaml:"trusted_scopes" json:"-"`
Insecure bool `yaml:"insecure" json:"-"`
ImplicitScopes []string `yaml:"implicit_scopes" json:"-"`
Dynamic bool `yaml:"-" json:"-"`
IDIssuedAt time.Time `yaml:"-" json:"-"`
SecretExpiresAt time.Time `yaml:"-" json:"-"`
Contacts []string `yaml:"contacts,flow" json:"contacts,omitempty"`
Name string `yaml:"name" json:"name,omitempty"`
URI string `yaml:"uri" json:"uri,omitempty"`
GrantTypes []string `yaml:"grant_types,flow" json:"grant_types,omitempty"`
ApplicationType string `yaml:"application_type" json:"application_type,omitempty"`
RedirectURIs []string `yaml:"redirect_uris,flow" json:"redirect_uris,omitempty"`
Origins []string `yaml:"origins,flow" json:"-"`
JWKS *gojwk.Key `yaml:"jwks" json:"-"`
RawIDTokenSignedResponseAlg string `yaml:"id_token_signed_response_alg" json:"id_token_signed_response_alg,omitempty"`
RawUserInfoSignedResponseAlg string `yaml:"userinfo_signed_response_alg" json:"userinfo_signed_response_alg,omitempty"`
RawRequestObjectSigningAlg string `yaml:"request_object_signing_alg" json:"request_object_signing_alg,omitempty"`
RawTokenEndpointAuthMethod string `yaml:"token_endpoint_auth_method" json:"token_endpoint_auth_method,omitempty"`
RawTokenEndpointAuthSigningAlg string `yaml:"token_endpoint_auth_signing_alg" json:"token_endpoint_auth_signing_alg,omitempty"`
PostLogoutRedirectURIs []string `yaml:"post_logout_redirect_uris,flow" json:"post_logout_redirect_uris,omitempty"`
}
// Validate validates the associated client registration data and returns error
// if the data is not valid.
func (cr *ClientRegistration) Validate() error {
return nil
}
// Secure looks up the a matching key from the accociated client registration
// and returns its public key part as a secured client.
func (cr *ClientRegistration) Secure(rawKid interface{}) (*Secured, error) {
var kid string
var key crypto.PublicKey
var err error
switch len(cr.JWKS.Keys) {
case 0:
// breaks
case 1:
// Use the one and only, no matter what kid says.
key, err = cr.JWKS.Keys[0].DecodePublicKey()
if err != nil {
return nil, err
}
kid = cr.JWKS.Keys[0].Kid
default:
// Find by kid.
kid, _ = rawKid.(string)
if kid == "" {
kid = "default"
}
for _, k := range cr.JWKS.Keys {
if kid == k.Kid {
key, err = k.DecodePublicKey()
if err != nil {
return nil, err
}
break
}
}
}
if key == nil {
return nil, fmt.Errorf("unknown kid")
}
return &Secured{
ID: cr.ID,
DisplayName: cr.Name,
ApplicationType: cr.ApplicationType,
Kid: kid,
PublicKey: key,
TrustedScopes: cr.TrustedScopes,
Registration: cr,
}, nil
}
// SetDynamic modifieds the required data for the associated client registration
// so it becomes a dynamic client.
func (cr *ClientRegistration) SetDynamic(ctx context.Context, creator func(ctx context.Context, signingMethod jwt.SigningMethod, claims jwt.Claims) (string, error)) error {
if creator == nil {
return fmt.Errorf("no creator")
}
if cr.ID != "" {
return fmt.Errorf("has ID already")
}
registry, ok := FromRegistryContext(ctx)
if !ok {
return fmt.Errorf("no registry")
}
// Initialize basic client registration data for dynamic client.
cr.IDIssuedAt = time.Now()
if registry.dynamicClientSecretDuration > 0 {
cr.SecretExpiresAt = time.Now().Add(registry.dynamicClientSecretDuration)
}
cr.Dynamic = true
sub, secret, err := cr.makeSecret(nil)
if err != nil {
return fmt.Errorf("failed to make dynamic client secret: %v", err)
}
// Stateless Dynamic Client Registration encodes all relevant data in the
// client_id. See https://openid.net/specs/openid-connect-registration-1_0.html#StatelessRegistration
// for more information. We use a JWT as client_id.
claims := &RegistrationClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: sub,
IssuedAt: jwt.NewNumericDate(cr.IDIssuedAt),
ExpiresAt: jwt.NewNumericDate(cr.SecretExpiresAt),
},
ClientRegistration: cr,
}
// Create signed stateless client ID by help of the provided creator function.
id, err := creator(ctx, nil, claims)
if err != nil {
return nil
}
// Fill in ID and secret.
cr.ID = DynamicStatelessClientIDPrefix + id
cr.Secret = secret
return nil
}
// ApplyImplicitScopes apples the associated registration's implicit scopes to
// the provided scopes map.
func (cr *ClientRegistration) ApplyImplicitScopes(scopes map[string]bool) error {
for _, scope := range cr.ImplicitScopes {
if scope != "" {
scopes[scope] = true
}
}
return nil
}
func (cr *ClientRegistration) makeSecret(secret []byte) (string, string, error) {
// Create random secret. HMAC the client name with it to get the subject.
if secret == nil {
secret = rndm.GenerateRandomBytes(64)
}
hasher, err := blake2b.New512(secret)
if err != nil {
return "", "", fmt.Errorf("failed to create hasher for dynamic client_id: %v", err)
}
hasher.Write([]byte(cr.Name))
hasher.Write([]byte(" "))
hasher.Write([]byte(DynamicStatelessClientStaticSaltV1))
sub := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return sub, base64.RawURLEncoding.EncodeToString(secret), nil
}
func (cr *ClientRegistration) validateSecret(clientSecret string) (bool, error) {
if cr.Dynamic {
if cr.Secret == "" {
// Fail fast, since dynamic clients must have a secret.
return false, fmt.Errorf("no secret in registration")
}
// Dynamic clients use hashed passwords.
secret, err := base64.RawURLEncoding.DecodeString(clientSecret)
if err != nil {
return false, fmt.Errorf("failed to decode client secret: %v", err)
}
sub, _, err := cr.makeSecret(secret)
if err != nil {
return false, fmt.Errorf("failed to produce client secret for comparison: %v", err)
}
return subtle.ConstantTimeCompare([]byte(sub), []byte(cr.Secret)) == 1, nil
}
if cr.Secret != "" && subtle.ConstantTimeCompare([]byte(clientSecret), []byte(cr.Secret)) != 1 {
return false, nil
}
return true, nil
}
+368
View File
@@ -0,0 +1,368 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package clients
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/url"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
// Registry implements the registry for registered clients.
type Registry struct {
mutex sync.RWMutex
trustedURI *url.URL
clients map[string]*ClientRegistration
allowDynamicClientRegistration bool
dynamicClientSecretDuration time.Duration
StatelessCreator func(ctx context.Context, signingMethod jwt.SigningMethod, claims jwt.Claims) (string, error)
StatelessValidator func(token *jwt.Token) (interface{}, error)
logger logrus.FieldLogger
}
// contextKey is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type contextKey int
// claimsKey is the key for claims in contexts. It is
// unexported; clients use konnect.NewClaimsContext and
// connect.FromClaimsContext instead of using this key directly.
var registryKey contextKey
// NewRegistry created a new client Registry with the provided parameters.
func NewRegistry(ctx context.Context, trustedURI *url.URL, registrationConfFilepath string, allowDynamicClientRegistration bool, dynamicClientSecretDuration time.Duration, logger logrus.FieldLogger) (*Registry, error) {
registryData := &RegistryData{}
if registrationConfFilepath != "" {
logger.Debugf("parsing identifier registration conf from %v", registrationConfFilepath)
registryFile, err := ioutil.ReadFile(registrationConfFilepath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(registryFile, registryData)
if err != nil {
return nil, err
}
}
r := &Registry{
trustedURI: trustedURI,
clients: make(map[string]*ClientRegistration),
allowDynamicClientRegistration: allowDynamicClientRegistration,
dynamicClientSecretDuration: dynamicClientSecretDuration,
logger: logger,
}
for _, client := range registryData.Clients {
validateErr := client.Validate()
registerErr := r.Register(client)
fields := logrus.Fields{
"client_id": client.ID,
"with_client_secret": client.Secret != "",
"trusted": client.Trusted,
"insecure": client.Insecure,
"application_type": client.ApplicationType,
"redirect_uris": client.RedirectURIs,
"origins": client.Origins,
}
if validateErr != nil {
logger.WithError(validateErr).WithFields(fields).Warnln("skipped registration of invalid client entry")
continue
}
if registerErr != nil {
logger.WithError(registerErr).WithFields(fields).Warnln("skipped registration of invalid client")
continue
}
logger.WithFields(fields).Debugln("registered client")
}
return r, nil
}
// NewRegistryContext returns a new Context that carries value provided Registry.
func NewRegistryContext(ctx context.Context, r *Registry) context.Context {
return context.WithValue(ctx, registryKey, r)
}
// FromRegistryContext returns the Registry value stored in ctx, if any.
func FromRegistryContext(ctx context.Context) (*Registry, bool) {
r, ok := ctx.Value(registryKey).(*Registry)
return r, ok
}
// Register validates the provided client registration and adds the client
// to the accociated registry if valid. Returns error otherwise.
func (r *Registry) Register(client *ClientRegistration) error {
if client.ID == "" {
return errors.New("invalid client_id")
}
if !client.Insecure && len(client.RedirectURIs) == 0 {
return errors.New("no redirect_uris")
}
switch client.ApplicationType {
case "":
client.ApplicationType = oidc.ApplicationTypeWeb
fallthrough
case oidc.ApplicationTypeWeb:
for _, urlString := range client.RedirectURIs {
// http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
parsed, _ := url.Parse(urlString)
if parsed == nil || parsed.Host == "" {
return fmt.Errorf("invalid redirect_uri %v - invalid or no hostname", urlString)
} else if !client.Insecure && parsed.Scheme != "https" {
return fmt.Errorf("invalid redirect_uri %v - make sure to use https when application_type is web", parsed)
} else if IsLocalNativeHTTPURI(parsed) {
return fmt.Errorf("invalid redirect_uri %v - host must not be localhost when application_type is web", parsed)
}
if len(client.Origins) == 0 {
// Auto add first redirect scheme and host as Origin if no
// origin is explicitly configured.
client.Origins = append(client.Origins, parsed.Scheme+"://"+parsed.Host)
}
}
if !client.Insecure && len(client.Origins) == 0 {
return errors.New("no origins - origin is required when application_type is web")
}
// breaks
case oidc.ApplicationTypeNative:
// breaks
for _, urlString := range client.RedirectURIs {
// http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
parsed, _ := url.Parse(urlString)
if parsed == nil || parsed.Host == "" {
return fmt.Errorf("invalid redirect_uri %v - invalid uri or no hostname", urlString)
} else if parsed.Scheme == "https" {
return fmt.Errorf("invalid redirect_uri %v - scheme must not be https when application_type is native", parsed)
} else if parsed.Scheme == "http" && !IsLocalNativeHTTPURI(parsed) {
return fmt.Errorf("invalid redirect_uri %v = http host must be localhost when application_type is native", parsed)
}
}
// breaks
default:
return fmt.Errorf("unknown application_type: %v", client.ApplicationType)
}
r.mutex.Lock()
defer r.mutex.Unlock()
r.clients[client.ID] = client
return nil
}
// Validate checks if the provided client registration data complies to the
// provided parameters and returns error when it does not.
func (r *Registry) Validate(client *ClientRegistration, clientSecret string, redirectURIString string, originURIString string, withoutSecret bool) error {
if client.ApplicationType == oidc.ApplicationTypeWeb {
if originURIString != "" && (!client.Insecure || len(client.Origins) > 0) {
// Compare originURI if it was given.
originOK := false
for _, urlString := range client.Origins {
if urlString == originURIString {
originOK = true
break
}
}
if !originOK {
return fmt.Errorf("invalid origin: %v", originURIString)
}
}
}
if redirectURIString != "" && (!client.Insecure || len(client.RedirectURIs) > 0) {
// Make sure to validate the redirect URI unless client is marked insecure
// and has no configured redirect URIs.
redirectURIOK := false
for _, registeredURIString := range client.RedirectURIs {
if client.ApplicationType == oidc.ApplicationTypeNative {
registeredURI, _ := url.Parse(registeredURIString)
if IsLocalNativeHTTPURI(registeredURI) {
redirectURI, err := url.Parse(redirectURIString)
if err != nil {
break
}
if IsLocalNativeHTTPURI(redirectURI) {
if registeredURI.Path == "" || redirectURI.Path == registeredURI.Path {
redirectURIOK = true
break
}
}
continue
}
}
if registeredURIString == redirectURIString {
redirectURIOK = true
break
}
}
if !redirectURIOK {
return fmt.Errorf("invalid redirect_uri: %v", redirectURIString)
}
}
if !withoutSecret {
if valid, err := client.validateSecret(clientSecret); !valid {
return fmt.Errorf("invalid client_secret: %v", err)
}
}
return nil
}
// Lookup returns and validates the clients Detail information for the provided
// parameters from the accociated registry.
func (r *Registry) Lookup(ctx context.Context, clientID string, clientSecret string, redirectURI *url.URL, originURIString string, withoutSecret bool) (*Details, error) {
var err error
var trusted bool
var dynamic bool
var displayName string
if clientID == "" {
return nil, fmt.Errorf("invalid client_id")
}
originURI, _ := url.Parse(originURIString)
// Implicit trust for web clients running and redirecting to the same origin
// as the issuer (ourselves).
if r.trustedURI != nil {
for {
if r.trustedURI.Scheme != redirectURI.Scheme || r.trustedURI.Host != redirectURI.Host {
break
}
if originURI.Scheme != "" && (r.trustedURI.Scheme != originURI.Scheme || r.trustedURI.Host != originURI.Host) {
break
}
trusted = true
break
}
}
// Lookup client registration.
r.mutex.RLock()
registration, _ := r.clients[clientID]
r.mutex.RUnlock()
if registration == nil && strings.HasPrefix(clientID, DynamicStatelessClientIDPrefix) {
trusted = false
dynamic = true
}
// Lookup dynamic clients when it makes sense.
if dynamic && registration == nil {
registration, _ = r.getDynamicClient(clientID)
}
if registration != nil {
redirectURIBase := &url.URL{
Scheme: redirectURI.Scheme,
Host: redirectURI.Host,
Path: redirectURI.Path,
}
err = r.Validate(registration, clientSecret, redirectURIBase.String(), originURIString, withoutSecret)
displayName = registration.Name
trusted = registration.Trusted
} else {
if trusted {
// Always let in implicitly trusted clients.
err = nil
} else {
err = fmt.Errorf("unknown client_id: %v", clientID)
}
}
if err != nil {
return nil, err
}
redirecURIString := redirectURI.String()
r.logger.WithFields(logrus.Fields{
"trusted": trusted,
"client_id": clientID,
"redirect_uri": redirecURIString,
"known": registration != nil,
}).Debugln("identifier client lookup")
return &Details{
ID: clientID,
RedirectURI: redirecURIString,
DisplayName: displayName,
Trusted: trusted,
Registration: registration,
}, nil
}
// Get returns the registered clients registration for the provided client ID.
func (r *Registry) Get(ctx context.Context, clientID string) (*ClientRegistration, bool) {
// Lookup client registration.
r.mutex.RLock()
registration, ok := r.clients[clientID]
r.mutex.RUnlock()
if ok {
return registration, true
}
return r.getDynamicClient(clientID)
}
func (r *Registry) getDynamicClient(clientID string) (*ClientRegistration, bool) {
var registration *ClientRegistration
if len(clientID) >= len(DynamicStatelessClientIDPrefix) {
tokenString := clientID[len(DynamicStatelessClientIDPrefix):]
if token, err := jwt.ParseWithClaims(tokenString, &RegistrationClaims{}, func(token *jwt.Token) (interface{}, error) {
if r.StatelessValidator == nil {
return nil, fmt.Errorf("no validator for dynamic client ids")
}
return r.StatelessValidator(token)
}); err == nil {
if claims, ok := token.Claims.(*RegistrationClaims); ok && token.Valid {
// TODO(longsleep): Add secure client secret.
registration = claims.ClientRegistration
registration.ID = clientID
registration.Secret = claims.RegisteredClaims.Subject
registration.Dynamic = true
}
}
}
return registration, registration != nil
}