Initial QSfera import
This commit is contained in:
+29
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user