Initial QSfera import
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 bsguest
|
||||
|
||||
import (
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
"github.com/libregraph/lico/identity"
|
||||
"github.com/libregraph/lico/identity/managers"
|
||||
)
|
||||
|
||||
// Identity managers.
|
||||
const (
|
||||
identityManagerName = "guest"
|
||||
)
|
||||
|
||||
func Register() error {
|
||||
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
|
||||
}
|
||||
|
||||
func MustRegister() {
|
||||
if err := Register(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
|
||||
config := bs.Config()
|
||||
|
||||
logger := config.Config.Logger
|
||||
|
||||
identityManagerConfig := &identity.Config{
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
guestIdentityManager := managers.NewGuestIdentityManager(identityManagerConfig)
|
||||
|
||||
return guestIdentityManager, nil
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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 bsldap
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
"github.com/libregraph/lico/identifier"
|
||||
"github.com/libregraph/lico/identifier/backends/ldap"
|
||||
"github.com/libregraph/lico/identity"
|
||||
"github.com/libregraph/lico/identity/managers"
|
||||
)
|
||||
|
||||
// Identity managers.
|
||||
const (
|
||||
identityManagerName = "ldap"
|
||||
)
|
||||
|
||||
func Register() error {
|
||||
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
|
||||
}
|
||||
|
||||
func MustRegister() {
|
||||
if err := Register(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
|
||||
config := bs.Config()
|
||||
|
||||
logger := config.Config.Logger
|
||||
|
||||
if config.AuthorizationEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("ldap backend is incompatible with authorization-endpoint-uri parameter")
|
||||
}
|
||||
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")
|
||||
|
||||
if config.EndSessionEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("ldap backend is incompatible with endsession-endpoint-uri parameter")
|
||||
}
|
||||
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")
|
||||
|
||||
if config.SignInFormURI.EscapedPath() == "" {
|
||||
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
|
||||
}
|
||||
|
||||
if config.SignedOutURI.EscapedPath() == "" {
|
||||
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
|
||||
}
|
||||
|
||||
// Default LDAP attribute mappings.
|
||||
attributeMapping := map[string]string{
|
||||
ldap.AttributeLogin: os.Getenv("LDAP_LOGIN_ATTRIBUTE"),
|
||||
ldap.AttributeEmail: os.Getenv("LDAP_EMAIL_ATTRIBUTE"),
|
||||
ldap.AttributeName: os.Getenv("LDAP_NAME_ATTRIBUTE"),
|
||||
ldap.AttributeFamilyName: os.Getenv("LDAP_FAMILY_NAME_ATTRIBUTE"),
|
||||
ldap.AttributeGivenName: os.Getenv("LDAP_GIVEN_NAME_ATTRIBUTE"),
|
||||
ldap.AttributeUUID: os.Getenv("LDAP_UUID_ATTRIBUTE"),
|
||||
fmt.Sprintf("%s_type", ldap.AttributeUUID): os.Getenv("LDAP_UUID_ATTRIBUTE_TYPE"),
|
||||
}
|
||||
// Add optional LDAP attribute mappings.
|
||||
if numericUIDAttribute := os.Getenv("LDAP_UIDNUMBER_ATTRIBUTE"); numericUIDAttribute != "" {
|
||||
attributeMapping[ldap.AttributeNumericUID] = numericUIDAttribute
|
||||
}
|
||||
// Sub from LDAP attribute mappings.
|
||||
var subMapping []string
|
||||
if subMappingString := os.Getenv("LDAP_SUB_ATTRIBUTES"); subMappingString != "" {
|
||||
subMapping = strings.Split(subMappingString, " ")
|
||||
}
|
||||
|
||||
// Use a clone here to avoid changing the config of other possible users of the config.
|
||||
tlsConfig := config.TLSClientConfig.Clone()
|
||||
if caCertFile := os.Getenv("LDAP_TLS_CACERT"); caCertFile != "" {
|
||||
if pemBytes, err := ioutil.ReadFile(caCertFile); err == nil {
|
||||
rpool, _ := x509.SystemCertPool()
|
||||
if rpool.AppendCertsFromPEM(pemBytes) {
|
||||
tlsConfig.RootCAs = rpool
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to append CA certificate(s) from '%s' to pool", caCertFile)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to read CA certificate(s) from '%s': %w", caCertFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
identifierBackend, identifierErr := ldap.NewLDAPIdentifierBackend(
|
||||
config.Config,
|
||||
tlsConfig,
|
||||
os.Getenv("LDAP_URI"),
|
||||
os.Getenv("LDAP_BINDDN"),
|
||||
os.Getenv("LDAP_BINDPW"),
|
||||
os.Getenv("LDAP_BASEDN"),
|
||||
os.Getenv("LDAP_SCOPE"),
|
||||
os.Getenv("LDAP_FILTER"),
|
||||
subMapping,
|
||||
attributeMapping,
|
||||
)
|
||||
if identifierErr != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
|
||||
}
|
||||
|
||||
fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
|
||||
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
|
||||
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)
|
||||
|
||||
activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
|
||||
Config: config.Config,
|
||||
|
||||
BaseURI: config.IssuerIdentifierURI,
|
||||
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
|
||||
StaticFolder: config.IdentifierClientPath,
|
||||
ScopesConf: config.IdentifierScopesConf,
|
||||
WebAppDisabled: config.IdentifierClientDisabled,
|
||||
|
||||
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
|
||||
LogonCookieSameSite: config.CookieSameSite,
|
||||
|
||||
ConsentCookieSameSite: config.CookieSameSite,
|
||||
|
||||
StateCookieSameSite: config.CookieSameSite,
|
||||
|
||||
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
|
||||
SignedOutEndpointURI: fullSignedOutEndpointURL,
|
||||
|
||||
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
|
||||
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
|
||||
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
|
||||
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
|
||||
UILocales: config.IdentifierUILocales,
|
||||
|
||||
Backend: identifierBackend,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier: %v", err)
|
||||
}
|
||||
err = activeIdentifier.SetKey(config.EncryptionSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
|
||||
}
|
||||
|
||||
identityManagerConfig := &identity.Config{
|
||||
SignInFormURI: fullSignInFormURL,
|
||||
SignedOutURI: fullSignedOutEndpointURL,
|
||||
|
||||
Logger: logger,
|
||||
|
||||
ScopesSupported: config.Config.AllowedScopes,
|
||||
}
|
||||
|
||||
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
|
||||
logger.Infoln("using identifier backed identity manager")
|
||||
|
||||
return identifierIdentityManager, nil
|
||||
}
|
||||
Generated
Vendored
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2021 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 bslibregraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cevaris/ordered_map"
|
||||
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
"github.com/libregraph/lico/identifier"
|
||||
"github.com/libregraph/lico/identifier/backends/libregraph"
|
||||
"github.com/libregraph/lico/identity"
|
||||
identityClients "github.com/libregraph/lico/identity/clients"
|
||||
"github.com/libregraph/lico/identity/managers"
|
||||
)
|
||||
|
||||
// Identity managers.
|
||||
const (
|
||||
identityManagerName = "libregraph"
|
||||
)
|
||||
|
||||
func Register() error {
|
||||
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
|
||||
}
|
||||
|
||||
func MustRegister() {
|
||||
if err := Register(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
|
||||
config := bs.Config()
|
||||
|
||||
logger := config.Config.Logger
|
||||
|
||||
if config.AuthorizationEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("libregraph backend is incompatible with authorization-endpoint-uri parameter")
|
||||
}
|
||||
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")
|
||||
|
||||
if config.EndSessionEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("libregraph backend is incompatible with endsession-endpoint-uri parameter")
|
||||
}
|
||||
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")
|
||||
|
||||
if config.SignInFormURI.EscapedPath() == "" {
|
||||
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
|
||||
}
|
||||
|
||||
if config.SignedOutURI.EscapedPath() == "" {
|
||||
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
|
||||
}
|
||||
|
||||
defaultURI := os.Getenv("LIBREGRAPH_URI")
|
||||
|
||||
var scopedURIs *ordered_map.OrderedMap
|
||||
if scopedURIsString := os.Getenv("LIBREGRAPH_SCOPED_URIS"); scopedURIsString != "" {
|
||||
scopedURIs = ordered_map.NewOrderedMap()
|
||||
// Format is <scope>:<url>,<scope>:<url>,...
|
||||
for _, v := range strings.Split(scopedURIsString, ",") {
|
||||
parts := strings.SplitN(v, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("failed to parse scoped URIs, format invalid")
|
||||
}
|
||||
scopedURIs.Set(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
var clients *identityClients.Registry
|
||||
if clientsRecord, ok := bs.Managers().Get("clients"); ok {
|
||||
clients = clientsRecord.(*identityClients.Registry)
|
||||
} else {
|
||||
return nil, fmt.Errorf("clients manager not found but is required")
|
||||
}
|
||||
|
||||
identifierBackend, identifierErr := libregraph.NewLibreGraphIdentifierBackend(
|
||||
config.Config,
|
||||
config.TLSClientConfig,
|
||||
defaultURI,
|
||||
scopedURIs,
|
||||
clients,
|
||||
)
|
||||
if identifierErr != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
|
||||
}
|
||||
|
||||
fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
|
||||
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
|
||||
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)
|
||||
|
||||
activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
|
||||
Config: config.Config,
|
||||
|
||||
BaseURI: config.IssuerIdentifierURI,
|
||||
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
|
||||
StaticFolder: config.IdentifierClientPath,
|
||||
ScopesConf: config.IdentifierScopesConf,
|
||||
WebAppDisabled: config.IdentifierClientDisabled,
|
||||
|
||||
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
|
||||
LogonCookieSameSite: config.CookieSameSite,
|
||||
|
||||
ConsentCookieSameSite: config.CookieSameSite,
|
||||
|
||||
StateCookieSameSite: config.CookieSameSite,
|
||||
|
||||
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
|
||||
SignedOutEndpointURI: fullSignedOutEndpointURL,
|
||||
|
||||
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
|
||||
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
|
||||
DefaultSignInPageLogoURI: config.IdentifierDefaultLogoTargetURI,
|
||||
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
|
||||
UILocales: config.IdentifierUILocales,
|
||||
|
||||
Backend: identifierBackend,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier: %v", err)
|
||||
}
|
||||
err = activeIdentifier.SetKey(config.EncryptionSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
|
||||
}
|
||||
|
||||
identityManagerConfig := &identity.Config{
|
||||
SignInFormURI: fullSignInFormURL,
|
||||
SignedOutURI: fullSignedOutEndpointURL,
|
||||
|
||||
Logger: logger,
|
||||
|
||||
ScopesSupported: config.Config.AllowedScopes,
|
||||
}
|
||||
|
||||
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
|
||||
logger.Infoln("using identifier backed identity manager")
|
||||
|
||||
return identifierIdentityManager, nil
|
||||
}
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/libregraph/lico/config"
|
||||
)
|
||||
|
||||
// Config is a typed application config which represents the active
|
||||
// bootstrap configuration.
|
||||
type Config struct {
|
||||
Config *config.Config
|
||||
Settings *Settings
|
||||
|
||||
SignInFormURI *url.URL
|
||||
SignedOutURI *url.URL
|
||||
AuthorizationEndpointURI *url.URL
|
||||
EndSessionEndpointURI *url.URL
|
||||
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
IssuerIdentifierURI *url.URL
|
||||
|
||||
IdentifierClientDisabled bool
|
||||
IdentifierClientPath string
|
||||
IdentifierRegistrationConf string
|
||||
IdentifierAuthoritiesConf string
|
||||
IdentifierScopesConf string
|
||||
IdentifierDefaultBannerLogo []byte
|
||||
IdentifierDefaultSignInPageText *string
|
||||
IdentifierDefaultLogoTargetURI *string
|
||||
IdentifierDefaultUsernameHintText *string
|
||||
IdentifierUILocales []string
|
||||
|
||||
EncryptionSecret []byte
|
||||
SigningMethod jwt.SigningMethod
|
||||
SigningKeyID string
|
||||
Signers map[string]crypto.Signer
|
||||
Validators map[string]crypto.PublicKey
|
||||
Certificates map[string][]*x509.Certificate
|
||||
|
||||
AccessTokenDurationSeconds uint64
|
||||
IDTokenDurationSeconds uint64
|
||||
RefreshTokenDurationSeconds uint64
|
||||
DyamicClientSecretDurationSeconds uint64
|
||||
|
||||
CookieSameSite http.SameSite
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/libregraph/lico/identity"
|
||||
identityAuthorities "github.com/libregraph/lico/identity/authorities"
|
||||
identityClients "github.com/libregraph/lico/identity/clients"
|
||||
identityManagers "github.com/libregraph/lico/identity/managers"
|
||||
"github.com/libregraph/lico/managers"
|
||||
codeManagers "github.com/libregraph/lico/oidc/code/managers"
|
||||
)
|
||||
|
||||
type IdentityManagerFactory func(Bootstrap) (identity.Manager, error)
|
||||
|
||||
var identityManagerRegistry = make(map[string]IdentityManagerFactory)
|
||||
|
||||
func RegisterIdentityManager(name string, f IdentityManagerFactory) error {
|
||||
identityManagerRegistry[name] = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIdentityManagerByName(name string, bs Bootstrap) (identity.Manager, error) {
|
||||
if f, found := identityManagerRegistry[name]; !found {
|
||||
return nil, fmt.Errorf("no identity manager with name %s registered", name)
|
||||
} else {
|
||||
return f(bs)
|
||||
}
|
||||
}
|
||||
|
||||
func newManagers(ctx context.Context, bs *bootstrap) (*managers.Managers, error) {
|
||||
logger := bs.config.Config.Logger
|
||||
|
||||
var err error
|
||||
mgrs := managers.New()
|
||||
|
||||
// Encryption manager.
|
||||
encryption, err := identityManagers.NewEncryptionManager(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create encryption manager: %v", err)
|
||||
}
|
||||
|
||||
err = encryption.SetKey(bs.config.EncryptionSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --encryption-secret parameter value for encryption: %v", err)
|
||||
}
|
||||
mgrs.Set("encryption", encryption)
|
||||
logger.Infof("encryption set up with %d key size", encryption.GetKeySize())
|
||||
|
||||
// OIDC code manage.
|
||||
code := codeManagers.NewMemoryMapManager(ctx)
|
||||
mgrs.Set("code", code)
|
||||
|
||||
// Identifier client registry manager.
|
||||
clients, err := identityClients.NewRegistry(ctx, bs.config.IssuerIdentifierURI, bs.config.IdentifierRegistrationConf, bs.config.Config.AllowDynamicClientRegistration, time.Duration(bs.config.DyamicClientSecretDurationSeconds)*time.Second, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client registry: %v", err)
|
||||
}
|
||||
mgrs.Set("clients", clients)
|
||||
|
||||
// Identifier authorities registry manager.
|
||||
authorities, err := identityAuthorities.NewRegistry(ctx, bs.MakeURI(APITypeSignin, ""), bs.config.IdentifierAuthoritiesConf, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create authorities registry: %v", err)
|
||||
}
|
||||
mgrs.Set("authorities", authorities)
|
||||
|
||||
return mgrs, nil
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Settings is a typed application config which represents the user accessible
|
||||
// boostrap settings params.
|
||||
type Settings struct {
|
||||
Iss string
|
||||
IdentityManager string
|
||||
URIBasePath string
|
||||
SignInURI string
|
||||
SignedOutURI string
|
||||
AuthorizationEndpointURI string
|
||||
EndsessionEndpointURI string
|
||||
Insecure bool
|
||||
TrustedProxy []string
|
||||
AllowScope []string
|
||||
AllowClientGuests bool
|
||||
AllowDynamicClientRegistration bool
|
||||
EncryptionSecretFile string
|
||||
Listen string
|
||||
IdentifierClientDisabled bool
|
||||
IdentifierClientPath string
|
||||
IdentifierRegistrationConf string
|
||||
IdentifierScopesConf string
|
||||
IdentifierDefaultBannerLogo string
|
||||
IdentifierDefaultSignInPageText string
|
||||
IdentifierDefaultLogoTargetURI string
|
||||
IdentifierDefaultUsernameHintText string
|
||||
IdentifierUILocales []string
|
||||
SigningKid string
|
||||
SigningMethod string
|
||||
SigningPrivateKeyFiles []string
|
||||
ValidationKeysPath string
|
||||
CookieBackendURI string
|
||||
CookieNames []string
|
||||
CookieSameSite http.SameSite
|
||||
AccessTokenDurationSeconds uint64
|
||||
IDTokenDurationSeconds uint64
|
||||
RefreshTokenDurationSeconds uint64
|
||||
DyamicClientSecretDurationSeconds uint64
|
||||
}
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func parseJSONWebKey(jsonBytes []byte) (*jose.JSONWebKey, error) {
|
||||
k := &jose.JSONWebKey{}
|
||||
if err := k.UnmarshalJSON(jsonBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// LoadSignerFromFile loads a private-key for signing
|
||||
//
|
||||
// Supports JSON (JWK/JWS) and PEM
|
||||
func LoadSignerFromFile(fn string) (string, crypto.Signer, error) {
|
||||
readBytes, errRead := ioutil.ReadFile(fn)
|
||||
if errRead != nil {
|
||||
return "", nil, fmt.Errorf("failed to parse key file: %v", errRead)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fn)
|
||||
switch ext {
|
||||
case ".json":
|
||||
k, err := parseJSONWebKey(readBytes)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
|
||||
}
|
||||
if !k.Valid() {
|
||||
return "", nil, fmt.Errorf("json file is not a valid JWK")
|
||||
}
|
||||
if k.IsPublic() {
|
||||
return "", nil, fmt.Errorf("JWK is a public key, private key required to use as signer")
|
||||
}
|
||||
signer, ok := k.Key.(crypto.Signer)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("JWS key type %T is not a signer", k.Key)
|
||||
}
|
||||
|
||||
return k.KeyID, signer, nil
|
||||
|
||||
case ".pem":
|
||||
fallthrough
|
||||
default:
|
||||
// Try PEM if not otherwise detected.
|
||||
signer, err := parsePEMSigner(readBytes)
|
||||
return "", signer, err
|
||||
}
|
||||
}
|
||||
|
||||
func parsePEMSigner(pemBytes []byte) (crypto.Signer, error) {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no PEM block found")
|
||||
}
|
||||
|
||||
var signer crypto.Signer
|
||||
for {
|
||||
pkcs1Key, errParse1 := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if errParse1 == nil {
|
||||
signer = pkcs1Key
|
||||
break
|
||||
}
|
||||
|
||||
pkcs8Key, errParse2 := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if errParse2 == nil {
|
||||
signerSigner, ok := pkcs8Key.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to use key as crypto signer")
|
||||
}
|
||||
signer = signerSigner
|
||||
break
|
||||
}
|
||||
|
||||
ecKey, errParse3 := x509.ParseECPrivateKey(block.Bytes)
|
||||
if errParse3 == nil {
|
||||
signer = ecKey
|
||||
break
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to parse signer key - valid PKCS#1, PKCS#8 ...? %v, %v, %v", errParse1, errParse2, errParse3)
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// LoadValidatorFromFile loads a public-key used for validation.
|
||||
//
|
||||
// Supported formats are JSON-JWK and PEM
|
||||
func LoadValidatorFromFile(fn string) (string, crypto.PublicKey, error) {
|
||||
kid, _, key, err := loadValidatorFromFile(fn)
|
||||
return kid, key, err
|
||||
}
|
||||
|
||||
// LoadCertificatesAndValidatorFromFile loads chain of certificates and a
|
||||
// public-key used for validation.
|
||||
//
|
||||
// Supported formats are JSON-JWK and PEM
|
||||
func LoadCertificatesAndValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
|
||||
return loadValidatorFromFile(fn)
|
||||
}
|
||||
|
||||
func loadValidatorFromFile(fn string) (string, []*x509.Certificate, crypto.PublicKey, error) {
|
||||
readBytes, errRead := ioutil.ReadFile(fn)
|
||||
if errRead != nil {
|
||||
return "", nil, nil, fmt.Errorf("failed to parse key file: %v", errRead)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fn)
|
||||
switch ext {
|
||||
case ".json":
|
||||
k, err := parseJSONWebKey(readBytes)
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("failed to parse key file as JWK: %v", err)
|
||||
}
|
||||
if !k.Valid() {
|
||||
return "", nil, nil, fmt.Errorf("json file is not a valid JWK")
|
||||
}
|
||||
if !k.IsPublic() {
|
||||
public := k.Public()
|
||||
k = &public
|
||||
}
|
||||
return k.KeyID, k.Certificates, k.Key, nil
|
||||
|
||||
case ".pem":
|
||||
fallthrough
|
||||
default:
|
||||
// Try PEM if not otherwise detected.
|
||||
certificates, validator, err := parsePEMValidator(readBytes)
|
||||
return "", certificates, validator, err
|
||||
}
|
||||
}
|
||||
|
||||
func parsePEMValidator(pemBytes []byte) ([]*x509.Certificate, crypto.PublicKey, error) {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, nil, fmt.Errorf("no PEM block found")
|
||||
}
|
||||
|
||||
var certificates []*x509.Certificate
|
||||
var validator crypto.PublicKey
|
||||
for {
|
||||
pkixPubKey, errParse0 := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if errParse0 == nil {
|
||||
validator = pkixPubKey
|
||||
break
|
||||
}
|
||||
|
||||
pkcs1PubKey, errParse1 := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
if errParse1 == nil {
|
||||
validator = pkcs1PubKey
|
||||
break
|
||||
}
|
||||
|
||||
pkcs1PrivKey, errParse2 := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if errParse2 == nil {
|
||||
validator = pkcs1PrivKey.Public()
|
||||
break
|
||||
}
|
||||
|
||||
pkcs8Key, errParse3 := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if errParse3 == nil {
|
||||
signerSigner, ok := pkcs8Key.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("failed to use key as crypto signer")
|
||||
}
|
||||
validator = signerSigner.Public()
|
||||
break
|
||||
}
|
||||
|
||||
ecKey, errParse4 := x509.ParseECPrivateKey(block.Bytes)
|
||||
if errParse4 == nil {
|
||||
validator = ecKey.Public()
|
||||
break
|
||||
}
|
||||
|
||||
certs, errParse5 := x509.ParseCertificates(block.Bytes)
|
||||
if errParse5 == nil {
|
||||
validator = certs[0].PublicKey
|
||||
certificates = append(certificates, certs...)
|
||||
break
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("failed to parse validator key - valid PKCS#1, PKCS#8 ...? %v, %v, %v, %v, %v, %v", errParse0, errParse1, errParse2, errParse3, errParse4, errParse5)
|
||||
}
|
||||
|
||||
return certificates, validator, nil
|
||||
}
|
||||
|
||||
func addSignerWithIDFromFile(fn string, kid string, bs *bootstrap) error {
|
||||
fi, err := os.Lstat(fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed load load signer key: %v", err)
|
||||
}
|
||||
|
||||
mode := fi.Mode()
|
||||
switch {
|
||||
case mode.IsDir():
|
||||
return fmt.Errorf("signer key must be a file")
|
||||
}
|
||||
|
||||
// Load file.
|
||||
signerKid, signer, err := LoadSignerFromFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if kid == "" {
|
||||
kid = signerKid
|
||||
}
|
||||
if kid == "" {
|
||||
// Get ID from file, following symbolic link.
|
||||
var real string
|
||||
if mode&os.ModeSymlink != 0 {
|
||||
real, err = os.Readlink(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, real = filepath.Split(real)
|
||||
} else {
|
||||
real = fi.Name()
|
||||
}
|
||||
|
||||
kid = getKeyIDFromFilename(real)
|
||||
}
|
||||
|
||||
if _, ok := bs.config.Signers[kid]; ok {
|
||||
bs.config.Config.Logger.WithFields(logrus.Fields{
|
||||
"path": fn,
|
||||
"kid": kid,
|
||||
}).Warnln("skipped as signer with same kid already loaded")
|
||||
return nil
|
||||
} else {
|
||||
bs.config.Config.Logger.WithFields(logrus.Fields{
|
||||
"path": fn,
|
||||
"kid": kid,
|
||||
}).Debugln("loaded signer key")
|
||||
}
|
||||
|
||||
bs.config.Signers[kid] = signer
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSigners(bs *bootstrap) error {
|
||||
haveRSA := false
|
||||
haveECDSA := false
|
||||
haveEd25519 := false
|
||||
for _, signer := range bs.config.Signers {
|
||||
switch s := signer.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
// Ensure the private key is not vulnerable with PKCS-1.5 signatures. See
|
||||
// https://paragonie.com/blog/2018/04/protecting-rsa-based-protocols-against-adaptive-chosen-ciphertext-attacks#rsa-anti-bb98
|
||||
// for details.
|
||||
if s.PublicKey.E < 65537 {
|
||||
return fmt.Errorf("RSA signing key with public exponent < 65537")
|
||||
}
|
||||
haveRSA = true
|
||||
case *ecdsa.PrivateKey:
|
||||
haveECDSA = true
|
||||
case ed25519.PrivateKey:
|
||||
haveEd25519 = true
|
||||
default:
|
||||
return fmt.Errorf("unsupported signer type: %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate signing method
|
||||
switch bs.config.SigningMethod.(type) {
|
||||
case *jwt.SigningMethodRSA:
|
||||
if !haveRSA {
|
||||
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
|
||||
}
|
||||
case *jwt.SigningMethodRSAPSS:
|
||||
if !haveRSA {
|
||||
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
|
||||
}
|
||||
case *jwt.SigningMethodECDSA:
|
||||
if !haveECDSA {
|
||||
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
|
||||
}
|
||||
case *jwt.SigningMethodEd25519:
|
||||
if !haveEd25519 {
|
||||
return fmt.Errorf("no private key for signing method: %s", bs.config.SigningMethod.Alg())
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported signing method: %s", bs.config.SigningMethod.Alg())
|
||||
}
|
||||
|
||||
if !haveRSA {
|
||||
bs.config.Config.Logger.Warnln("no RSA signing private key, some clients might not be compatible")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addValidatorsFromPath(pn string, bs *bootstrap) error {
|
||||
fi, err := os.Lstat(pn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed load load validator keys: %v", err)
|
||||
}
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
// OK.
|
||||
default:
|
||||
return fmt.Errorf("validator path must be a directory")
|
||||
}
|
||||
|
||||
// Load all files.
|
||||
files := []string{}
|
||||
if pemFiles, err := filepath.Glob(filepath.Join(pn, "*.pem")); err != nil {
|
||||
return fmt.Errorf("validator path err: %v", err)
|
||||
} else {
|
||||
files = append(files, pemFiles...)
|
||||
}
|
||||
if jsonFiles, err := filepath.Glob(filepath.Join(pn, "*.json")); err != nil {
|
||||
return fmt.Errorf("validator path err: %v", err)
|
||||
} else {
|
||||
files = append(files, jsonFiles...)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
kid, certificates, validator, err := loadValidatorFromFile(file)
|
||||
if err != nil {
|
||||
bs.config.Config.Logger.WithError(err).WithField("path", file).Warnln("failed to load validator key")
|
||||
continue
|
||||
}
|
||||
|
||||
// Get ID from file, without following symbolic links.
|
||||
if kid == "" {
|
||||
_, fn := filepath.Split(file)
|
||||
kid = getKeyIDFromFilename(fn)
|
||||
}
|
||||
if _, ok := bs.config.Validators[kid]; ok {
|
||||
bs.config.Config.Logger.WithFields(logrus.Fields{
|
||||
"path": file,
|
||||
"kid": kid,
|
||||
}).Warnln("skipped as validator with same kid already loaded")
|
||||
continue
|
||||
} else {
|
||||
bs.config.Config.Logger.WithFields(logrus.Fields{
|
||||
"path": file,
|
||||
"kid": kid,
|
||||
}).Debugln("loaded validator key")
|
||||
}
|
||||
bs.config.Validators[kid] = validator
|
||||
if certificates != nil {
|
||||
bs.config.Certificates[kid] = certificates
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithSchemeAndHost(u, base *url.URL) *url.URL {
|
||||
if u.Host != "" && u.Scheme != "" {
|
||||
return u
|
||||
}
|
||||
|
||||
r, _ := url.Parse(u.String())
|
||||
r.Scheme = base.Scheme
|
||||
r.Host = base.Host
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func getKeyIDFromFilename(fn string) string {
|
||||
ext := filepath.Ext(fn)
|
||||
return strings.TrimSuffix(fn, ext)
|
||||
}
|
||||
|
||||
func getCommonURLPathPrefix(p1, p2 string) (string, error) {
|
||||
parts1 := strings.Split(p1, "/")
|
||||
parts2 := strings.Split(p2, "/")
|
||||
|
||||
common := make([]string, 0)
|
||||
for idx, p := range parts1 {
|
||||
if idx >= len(parts2) {
|
||||
break
|
||||
}
|
||||
if p != parts2[idx] {
|
||||
break
|
||||
}
|
||||
common = append(common, p)
|
||||
}
|
||||
if len(common) == 0 {
|
||||
return "", errors.New("no common path prefix")
|
||||
}
|
||||
|
||||
return strings.Join(common, "/"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user