/* * 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 }