Files
opencloud/vendor/github.com/libregraph/lico/bootstrap/bootstrap.go
Christian Richter 16307e036d add new property IdentifierDefaultLogoTargetURI
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-04-28 13:36:13 +02:00

555 lines
18 KiB
Go

/*
* 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 bootstrap
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/config"
"github.com/libregraph/lico/encryption"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/managers"
oidcProvider "github.com/libregraph/lico/oidc/provider"
"github.com/libregraph/lico/utils"
)
// API types.
type APIType string
const (
APITypeKonnect APIType = "konnect"
APITypeSignin APIType = "signin"
)
// Defaults.
const (
DefaultSigningKeyID = "default"
DefaultSigningKeyBits = 2048
DefaultGuestIdentityManagerName = "guest"
DefaultCookieSameSite = http.SameSiteNoneMode
)
// Bootstrap is a data structure to hold configuration required to start
// konnectd.
type Bootstrap interface {
Config() *Config
Managers() *managers.Managers
MakeURIPath(api APIType, subpath string) string
}
// Implementation of the bootstrap interface.
type bootstrap struct {
config *Config
uriBasePath string
managers *managers.Managers
}
// Config returns the bootstap configuration.
func (bs *bootstrap) Config() *Config {
return bs.config
}
// Managers returns bootstrapped identity-managers.
func (bs *bootstrap) Managers() *managers.Managers {
return bs.managers
}
// Boot is the main entry point to bootstrap the service after validating the
// given configuration. The resulting Bootstrap struct can be used to retrieve
// configured identity-managers and their respective http-handlers and config.
//
// This function should be used by consumers which want to embed this project
// as a library.
func Boot(ctx context.Context, settings *Settings, cfg *config.Config) (Bootstrap, error) {
// NOTE(longsleep): Ensure to use same salt length as the hash size.
// See https://www.ietf.org/mail-archive/web/jose/current/msg02901.html for
// reference and https://github.com/golang-jwt/jwt/v4/issues/285 for
// the issue in upstream jwt-go.
for _, alg := range []string{jwt.SigningMethodPS256.Name, jwt.SigningMethodPS384.Name, jwt.SigningMethodPS512.Name} {
sm := jwt.GetSigningMethod(alg)
if signingMethodRSAPSS, ok := sm.(*jwt.SigningMethodRSAPSS); ok {
signingMethodRSAPSS.Options.SaltLength = rsa.PSSSaltLengthEqualsHash
}
}
bs := &bootstrap{
config: &Config{
Config: cfg,
Settings: settings,
},
}
err := bs.initialize(settings)
if err != nil {
return nil, err
}
err = bs.setup(ctx, settings)
if err != nil {
return nil, err
}
return bs, nil
}
// initialize, parsed parameters from commandline with validation and adds them
// to the associated Bootstrap data.
func (bs *bootstrap) initialize(settings *Settings) error {
logger := bs.config.Config.Logger
var err error
if settings.IdentityManager == "" {
return fmt.Errorf("identity-manager argument missing, use one of kc, ldap, cookie, dummy")
}
bs.config.IssuerIdentifierURI, err = url.Parse(settings.Iss)
if err != nil {
return fmt.Errorf("invalid iss value, iss is not a valid URL), %v", err)
} else if settings.Iss == "" {
return fmt.Errorf("missing iss value, did you provide the --iss parameter?")
} else if bs.config.IssuerIdentifierURI.Scheme != "https" {
return fmt.Errorf("invalid iss value, URL must start with https://")
} else if bs.config.IssuerIdentifierURI.Host == "" {
return fmt.Errorf("invalid iss value, URL must have a host")
}
bs.uriBasePath = settings.URIBasePath
bs.config.SignInFormURI, err = url.Parse(settings.SignInURI)
if err != nil {
return fmt.Errorf("invalid sign-in URI, %v", err)
}
bs.config.SignedOutURI, err = url.Parse(settings.SignedOutURI)
if err != nil {
return fmt.Errorf("invalid signed-out URI, %v", err)
}
bs.config.AuthorizationEndpointURI, err = url.Parse(settings.AuthorizationEndpointURI)
if err != nil {
return fmt.Errorf("invalid authorization-endpoint-uri, %v", err)
}
bs.config.EndSessionEndpointURI, err = url.Parse(settings.EndsessionEndpointURI)
if err != nil {
return fmt.Errorf("invalid endsession-endpoint-uri, %v", err)
}
if settings.Insecure {
// NOTE(longsleep): This disable http2 client support. See https://github.com/golang/go/issues/14275 for reasons.
bs.config.TLSClientConfig = utils.InsecureSkipVerifyTLSConfig()
logger.Warnln("insecure mode, TLS client connections are susceptible to man-in-the-middle attacks")
} else {
bs.config.TLSClientConfig = utils.DefaultTLSConfig()
}
for _, trustedProxy := range settings.TrustedProxy {
if ip := net.ParseIP(trustedProxy); ip != nil {
bs.config.Config.TrustedProxyIPs = append(bs.config.Config.TrustedProxyIPs, &ip)
continue
}
if _, ipNet, errParseCIDR := net.ParseCIDR(trustedProxy); errParseCIDR == nil {
bs.config.Config.TrustedProxyNets = append(bs.config.Config.TrustedProxyNets, ipNet)
continue
}
}
if len(bs.config.Config.TrustedProxyIPs) > 0 {
logger.Infoln("trusted proxy IPs", bs.config.Config.TrustedProxyIPs)
}
if len(bs.config.Config.TrustedProxyNets) > 0 {
logger.Infoln("trusted proxy networks", bs.config.Config.TrustedProxyNets)
}
if len(settings.AllowScope) > 0 {
bs.config.Config.AllowedScopes = settings.AllowScope
logger.Infoln("using custom allowed OAuth 2 scopes", bs.config.Config.AllowedScopes)
}
bs.config.Config.AllowClientGuests = settings.AllowClientGuests
if bs.config.Config.AllowClientGuests {
logger.Infoln("client controlled guests are enabled")
}
bs.config.Config.AllowDynamicClientRegistration = settings.AllowDynamicClientRegistration
if bs.config.Config.AllowDynamicClientRegistration {
logger.Infoln("dynamic client registration is enabled")
}
encryptionSecretFn := settings.EncryptionSecretFile
if encryptionSecretFn != "" {
logger.WithField("file", encryptionSecretFn).Infoln("loading encryption secret from file")
bs.config.EncryptionSecret, err = ioutil.ReadFile(encryptionSecretFn)
if err != nil {
return fmt.Errorf("failed to load encryption secret from file: %v", err)
}
if len(bs.config.EncryptionSecret) != encryption.KeySize {
return fmt.Errorf("invalid encryption secret size - must be %d bytes", encryption.KeySize)
}
} else {
logger.Warnf("missing --encryption-secret parameter, using random encyption secret with %d bytes", encryption.KeySize)
bs.config.EncryptionSecret = rndm.GenerateRandomBytes(encryption.KeySize)
}
bs.config.Config.ListenAddr = settings.Listen
bs.config.IdentifierClientDisabled = settings.IdentifierClientDisabled
bs.config.IdentifierClientPath = settings.IdentifierClientPath
bs.config.IdentifierRegistrationConf = settings.IdentifierRegistrationConf
if bs.config.IdentifierRegistrationConf != "" {
bs.config.IdentifierRegistrationConf, _ = filepath.Abs(bs.config.IdentifierRegistrationConf)
if _, errStat := os.Stat(bs.config.IdentifierRegistrationConf); errStat != nil {
return fmt.Errorf("identifier-registration-conf file not found or unable to access: %v", errStat)
}
bs.config.IdentifierAuthoritiesConf = bs.config.IdentifierRegistrationConf
}
bs.config.IdentifierScopesConf = settings.IdentifierScopesConf
if bs.config.IdentifierScopesConf != "" {
bs.config.IdentifierScopesConf, _ = filepath.Abs(bs.config.IdentifierScopesConf)
if _, errStat := os.Stat(bs.config.IdentifierScopesConf); errStat != nil {
return fmt.Errorf("identifier-scopes-conf file not found or unable to access: %v", errStat)
}
}
if settings.IdentifierDefaultBannerLogo != "" {
// Load from file.
b, errRead := ioutil.ReadFile(settings.IdentifierDefaultBannerLogo)
if errRead != nil {
return fmt.Errorf("identifier-default-banner-logo failed to open: %w", errRead)
}
bs.config.IdentifierDefaultBannerLogo = b
}
if settings.IdentifierDefaultSignInPageText != "" {
bs.config.IdentifierDefaultSignInPageText = &settings.IdentifierDefaultSignInPageText
}
if settings.IdentifierDefaultLogoTargetURI != "" {
bs.config.IdentifierDefaultLogoTargetURI = &settings.IdentifierDefaultLogoTargetURI
}
if settings.IdentifierDefaultUsernameHintText != "" {
bs.config.IdentifierDefaultUsernameHintText = &settings.IdentifierDefaultUsernameHintText
}
bs.config.IdentifierUILocales = settings.IdentifierUILocales
bs.config.SigningKeyID = settings.SigningKid
bs.config.Signers = make(map[string]crypto.Signer)
bs.config.Validators = make(map[string]crypto.PublicKey)
bs.config.Certificates = make(map[string][]*x509.Certificate)
signingMethodString := settings.SigningMethod
bs.config.SigningMethod = jwt.GetSigningMethod(signingMethodString)
if bs.config.SigningMethod == nil {
return fmt.Errorf("unknown signing method: %s", signingMethodString)
}
signingKeyFns := settings.SigningPrivateKeyFiles
if len(signingKeyFns) > 0 {
first := true
for _, signingKeyFn := range signingKeyFns {
logger.WithField("path", signingKeyFn).Infoln("loading signing key")
err = addSignerWithIDFromFile(signingKeyFn, "", bs)
if err != nil {
return err
}
if first {
// Also add key under the provided id.
first = false
err = addSignerWithIDFromFile(signingKeyFn, bs.config.SigningKeyID, bs)
if err != nil {
return err
}
}
}
} else {
//NOTE(longsleep): remove me - create keypair a random key pair.
sm := jwt.SigningMethodPS256
bs.config.SigningMethod = sm
logger.WithField("alg", sm.Name).Warnf("missing --signing-private-key parameter, using random %d bit signing key", DefaultSigningKeyBits)
signer, _ := rsa.GenerateKey(rand.Reader, DefaultSigningKeyBits)
bs.config.Signers[bs.config.SigningKeyID] = signer
}
// Ensure we have a signer for the things we need.
err = validateSigners(bs)
if err != nil {
return err
}
validationKeysPath := settings.ValidationKeysPath
if validationKeysPath != "" {
logger.WithField("path", validationKeysPath).Infoln("loading validation keys")
err = addValidatorsFromPath(validationKeysPath, bs)
if err != nil {
return err
}
}
bs.config.Config.HTTPTransport = utils.HTTPTransportWithTLSClientConfig(bs.config.TLSClientConfig)
bs.config.AccessTokenDurationSeconds = settings.AccessTokenDurationSeconds
if bs.config.AccessTokenDurationSeconds == 0 {
bs.config.AccessTokenDurationSeconds = 60 * 10 // 10 Minutes
}
bs.config.IDTokenDurationSeconds = settings.IDTokenDurationSeconds
if bs.config.IDTokenDurationSeconds == 0 {
bs.config.IDTokenDurationSeconds = 60 * 60 // 1 Hour
}
bs.config.RefreshTokenDurationSeconds = settings.RefreshTokenDurationSeconds
if bs.config.RefreshTokenDurationSeconds == 0 {
bs.config.RefreshTokenDurationSeconds = 60 * 60 * 24 * 365 * 3 // 3 Years
}
bs.config.DyamicClientSecretDurationSeconds = settings.DyamicClientSecretDurationSeconds
// add setting to allow setting the same site attribute of the cookies
bs.config.CookieSameSite = settings.CookieSameSite
if bs.config.CookieSameSite == 0 {
bs.config.CookieSameSite = DefaultCookieSameSite
}
return nil
}
// setup takes care of setting up the managers based on the associated
// Bootstrap's data.
func (bs *bootstrap) setup(ctx context.Context, settings *Settings) error {
managers, err := newManagers(ctx, bs)
if err != nil {
return err
}
bs.managers = managers
identityManager, err := bs.setupIdentity(ctx, settings)
if err != nil {
return err
}
managers.Set("identity", identityManager)
guestManager, err := bs.setupGuest(ctx, identityManager)
if err != nil {
return err
}
managers.Set("guest", guestManager)
oidcProvider, err := bs.setupOIDCProvider(ctx)
if err != nil {
return err
}
managers.Set("oidc", oidcProvider)
managers.Set("handler", oidcProvider) // Use OIDC provider as default HTTP handler.
err = managers.Apply()
if err != nil {
return fmt.Errorf("failed to apply managers: %v", err)
}
// Final steps
err = oidcProvider.InitializeMetadata()
if err != nil {
return fmt.Errorf("failed to initialize provider metadata: %v", err)
}
return nil
}
func (bs *bootstrap) MakeURIPath(api APIType, subpath string) string {
subpath = strings.TrimPrefix(subpath, "/")
uriPath := ""
switch api {
case APITypeKonnect:
uriPath = fmt.Sprintf("%s/konnect/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
case APITypeSignin:
uriPath = fmt.Sprintf("%s/signin/v1/%s", strings.TrimSuffix(bs.uriBasePath, "/"), subpath)
default:
panic("unknown api type")
}
if subpath == "" {
uriPath = strings.TrimSuffix(uriPath, "/")
}
return uriPath
}
func (bs *bootstrap) MakeURI(api APIType, subpath string) *url.URL {
uriPath := bs.MakeURIPath(api, subpath)
uri, _ := url.Parse(bs.config.IssuerIdentifierURI.String())
uri.Path = uriPath
return uri
}
func (bs *bootstrap) setupIdentity(ctx context.Context, settings *Settings) (identity.Manager, error) {
logger := bs.config.Config.Logger
if settings.IdentityManager == "" {
return nil, fmt.Errorf("identity-manager argument missing")
}
// Identity manager.
identityManagerName := settings.IdentityManager
identityManager, err := getIdentityManagerByName(identityManagerName, bs)
if err != nil {
return nil, err
}
logger.WithFields(logrus.Fields{
"name": identityManagerName,
"scopes": identityManager.ScopesSupported(nil),
"claims": identityManager.ClaimsSupported(nil),
}).Infoln("identity manager set up")
return identityManager, nil
}
func (bs *bootstrap) setupGuest(ctx context.Context, identityManager identity.Manager) (identity.Manager, error) {
if !bs.config.Config.AllowClientGuests {
return nil, nil
}
var err error
logger := bs.config.Config.Logger
guestManager, err := getIdentityManagerByName(DefaultGuestIdentityManagerName, bs)
if err != nil {
return nil, err
}
if guestManager != nil {
logger.Infoln("identity guest manager set up")
}
return guestManager, nil
}
func (bs *bootstrap) setupOIDCProvider(ctx context.Context) (*oidcProvider.Provider, error) {
var err error
logger := bs.config.Config.Logger
sessionCookiePath, err := getCommonURLPathPrefix(bs.config.AuthorizationEndpointURI.EscapedPath(), bs.config.EndSessionEndpointURI.EscapedPath())
if err != nil {
return nil, fmt.Errorf("failed to find common URL prefix for authorize and endsession: %v", err)
}
var registrationPath = ""
if bs.config.Config.AllowDynamicClientRegistration {
registrationPath = bs.MakeURIPath(APITypeKonnect, "/register")
}
provider, err := oidcProvider.NewProvider(&oidcProvider.Config{
Config: bs.config.Config,
IssuerIdentifier: bs.config.IssuerIdentifierURI.String(),
WellKnownPath: "/.well-known/openid-configuration",
JwksPath: bs.MakeURIPath(APITypeKonnect, "/jwks.json"),
AuthorizationPath: bs.config.AuthorizationEndpointURI.EscapedPath(),
TokenPath: bs.MakeURIPath(APITypeKonnect, "/token"),
UserInfoPath: bs.MakeURIPath(APITypeKonnect, "/userinfo"),
EndSessionPath: bs.config.EndSessionEndpointURI.EscapedPath(),
CheckSessionIframePath: bs.MakeURIPath(APITypeKonnect, "/session/check-session.html"),
RegistrationPath: registrationPath,
BrowserStateCookiePath: bs.MakeURIPath(APITypeKonnect, "/session/"),
BrowserStateCookieName: "__Secure-KKBS", // Kopano-Konnect-Browser-State
BrowserStateCookieSameSite: bs.config.CookieSameSite,
SessionCookiePath: sessionCookiePath,
SessionCookieName: "__Secure-KKCS", // Kopano-Konnect-Client-Session
SessionCookieSameSite: bs.config.CookieSameSite,
AccessTokenDuration: time.Duration(bs.config.AccessTokenDurationSeconds) * time.Second,
IDTokenDuration: time.Duration(bs.config.IDTokenDurationSeconds) * time.Second,
RefreshTokenDuration: time.Duration(bs.config.RefreshTokenDurationSeconds) * time.Second,
})
if err != nil {
return nil, fmt.Errorf("failed to create provider: %v", err)
}
if bs.config.SigningMethod != nil {
err = provider.SetSigningMethod(bs.config.SigningMethod)
if err != nil {
return nil, fmt.Errorf("failed to set provider signing method: %v", err)
}
}
// All add signers.
for id, signer := range bs.config.Signers {
if id == bs.config.SigningKeyID {
err = provider.SetSigningKey(id, signer)
// Always set default key.
if id != DefaultSigningKeyID {
provider.SetValidationKey(DefaultSigningKeyID, signer.Public())
}
} else {
// Set non default signers as well.
err = provider.SetSigningKey(id, signer)
}
if err != nil {
return nil, err
}
}
// Add all validators.
for id, publicKey := range bs.config.Validators {
err = provider.SetValidationKey(id, publicKey)
if err != nil {
return nil, err
}
}
// Add all certificates.
for id, certificate := range bs.config.Certificates {
err = provider.SetCertificate(id, certificate)
if err != nil {
return nil, err
}
}
sk, ok := provider.GetSigningKey(bs.config.SigningMethod)
if !ok {
return nil, fmt.Errorf("no signing key for selected signing method")
}
if bs.config.SigningKeyID == "" {
// Ensure that there is a default signing Key ID even if none was set.
provider.SetValidationKey(DefaultSigningKeyID, sk.PrivateKey.Public())
}
logger.WithFields(logrus.Fields{
"id": sk.ID,
"method": fmt.Sprintf("%T", sk.SigningMethod),
"alg": sk.SigningMethod.Alg(),
}).Infoln("oidc token signing default set up")
return provider, nil
}