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