-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IDP: directly use CS3 API to authenticate users
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
- Loading branch information
Showing
12 changed files
with
555 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
Enhancement: Directly authenticate users via CS3 | ||
|
||
The IDP now directly authenticates users using the CS3 API instead of LDAP. | ||
|
||
https://github.com/owncloud/ocis/pull/3825 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* | ||
* 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 bootstrap | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
|
||
"github.com/libregraph/lico/bootstrap" | ||
"github.com/libregraph/lico/identifier" | ||
"github.com/libregraph/lico/identity" | ||
"github.com/libregraph/lico/identity/managers" | ||
cs3 "github.com/owncloud/ocis/v2/extensions/idp/pkg/backends/cs3/identifier" | ||
) | ||
|
||
// Identity managers. | ||
const ( | ||
identityManagerName = "cs3" | ||
) | ||
|
||
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("cs3 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("cs3 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") | ||
} | ||
|
||
identifierBackend, identifierErr := cs3.NewCS3Backend( | ||
config.Config, | ||
config.TLSClientConfig, | ||
os.Getenv("CS3_GATEWAY"), // FIXME how do we pass custom config to backends? | ||
os.Getenv("CS3_MACHINE_AUTH_API_KEY"), // FIXME how do we pass custom config to backends? | ||
config.Settings.Insecure, | ||
) | ||
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, | ||
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token | ||
ScopesConf: config.IdentifierScopesConf, | ||
WebAppDisabled: config.IdentifierClientDisabled, | ||
|
||
AuthorizationEndpointURI: fullAuthorizationEndpointURL, | ||
SignedOutEndpointURI: fullSignedOutEndpointURL, | ||
|
||
DefaultBannerLogo: config.IdentifierDefaultBannerLogo, | ||
DefaultSignInPageText: config.IdentifierDefaultSignInPageText, | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
package cs3 | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
|
||
cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" | ||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" | ||
"github.com/libregraph/lico" | ||
"github.com/libregraph/lico/config" | ||
"github.com/libregraph/lico/identifier/backends" | ||
"github.com/libregraph/lico/identifier/meta/scopes" | ||
"github.com/libregraph/lico/identity" | ||
cmap "github.com/orcaman/concurrent-map" | ||
"github.com/sirupsen/logrus" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
ins "google.golang.org/grpc/credentials/insecure" | ||
"stash.kopano.io/kgol/oidc-go" | ||
) | ||
|
||
const cs3BackendName = "identifier-cs3" | ||
|
||
var cs3SpportedScopes = []string{ | ||
oidc.ScopeProfile, | ||
oidc.ScopeEmail, | ||
lico.ScopeUniqueUserID, | ||
lico.ScopeRawSubject, | ||
} | ||
|
||
type CS3Backend struct { | ||
supportedScopes []string | ||
|
||
logger logrus.FieldLogger | ||
tlsConfig *tls.Config | ||
gatewayURI string | ||
machineAuthAPIKey string | ||
insecure bool | ||
|
||
sessions cmap.ConcurrentMap | ||
|
||
gateway cs3gateway.GatewayAPIClient | ||
} | ||
|
||
func NewCS3Backend( | ||
c *config.Config, | ||
tlsConfig *tls.Config, | ||
gatewayURI string, | ||
machineAuthAPIKey string, | ||
insecure bool, | ||
) (*CS3Backend, error) { | ||
|
||
// Build supported scopes based on default scopes. | ||
supportedScopes := make([]string, len(cs3SpportedScopes)) | ||
copy(supportedScopes, cs3SpportedScopes) | ||
|
||
b := &CS3Backend{ | ||
supportedScopes: supportedScopes, | ||
|
||
logger: c.Logger, | ||
tlsConfig: tlsConfig, | ||
gatewayURI: gatewayURI, | ||
machineAuthAPIKey: machineAuthAPIKey, | ||
insecure: insecure, | ||
|
||
sessions: cmap.New(), | ||
} | ||
|
||
b.logger.Infoln("cs3 backend connection set up") | ||
|
||
return b, nil | ||
} | ||
|
||
// RunWithContext implements the Backend interface. | ||
func (b *CS3Backend) RunWithContext(ctx context.Context) error { | ||
return nil | ||
} | ||
|
||
// Logon implements the Backend interface, enabling Logon with user name and | ||
// password as provided. Requests are bound to the provided context. | ||
func (b *CS3Backend) Logon(ctx context.Context, audience, username, password string) (bool, *string, *string, backends.UserFromBackend, error) { | ||
|
||
l, err := b.connect(ctx) | ||
if err != nil { | ||
return false, nil, nil, nil, fmt.Errorf("cs3 backend logon connect error: %v", err) | ||
} | ||
defer l.Close() | ||
|
||
client := cs3gateway.NewGatewayAPIClient(l) | ||
|
||
res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{ | ||
Type: "basic", | ||
ClientId: username, | ||
ClientSecret: password, | ||
}) | ||
if err != nil || res.Status.Code != cs3rpc.Code_CODE_OK { | ||
return false, nil, nil, nil, nil | ||
} | ||
res2, err := client.WhoAmI(ctx, &cs3gateway.WhoAmIRequest{ | ||
Token: res.Token, | ||
}) | ||
if err != nil || res2.Status.Code != cs3rpc.Code_CODE_OK { | ||
return false, nil, nil, nil, nil | ||
} | ||
|
||
session, _ := createSession(ctx, res2.User) | ||
|
||
user, err := newCS3User(res2.User) | ||
if err != nil { | ||
return false, nil, nil, nil, fmt.Errorf("cs3 backend resolve entry data error: %v", err) | ||
} | ||
|
||
// Use the users subject as user id. | ||
userID := user.Subject() | ||
|
||
sessionRef := identity.GetSessionRef(b.Name(), audience, userID) | ||
b.sessions.Set(*sessionRef, session) | ||
b.logger.WithFields(logrus.Fields{ | ||
"session": session, | ||
"ref": *sessionRef, | ||
"username": user.Username(), | ||
"id": userID, | ||
}).Debugln("cs3 backend logon") | ||
|
||
return true, &userID, sessionRef, user, nil | ||
} | ||
|
||
// GetUser implements the Backend interface, providing user meta data retrieval | ||
// for the user specified by the userID. Requests are bound to the provided | ||
// context. | ||
func (b *CS3Backend) GetUser(ctx context.Context, userEntryID string, sessionRef *string, requestedScopes map[string]bool) (backends.UserFromBackend, error) { | ||
|
||
session, err := b.getSessionForUser(ctx, userEntryID, sessionRef, true, true, false) | ||
if err != nil { | ||
return nil, fmt.Errorf("cs3 backend resolve session error: %v", err) | ||
} | ||
|
||
user, err := newCS3User(session.User()) | ||
if err != nil { | ||
return nil, fmt.Errorf("cs3 backend get user failed to process user: %v", err) | ||
} | ||
// TODO double check userEntryID matches session? | ||
|
||
return user, nil | ||
} | ||
|
||
// ResolveUserByUsername implements the Backend interface, providing lookup for | ||
// user by providing the username. Requests are bound to the provided context. | ||
func (b *CS3Backend) ResolveUserByUsername(ctx context.Context, username string) (backends.UserFromBackend, error) { | ||
|
||
l, err := b.connect(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("cs3 backend resolve username connect error: %v", err) | ||
} | ||
defer l.Close() | ||
|
||
client := cs3gateway.NewGatewayAPIClient(l) | ||
|
||
res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{ | ||
Type: "machine", | ||
ClientId: "username:" + username, | ||
ClientSecret: b.machineAuthAPIKey, | ||
}) | ||
if err != nil || res.Status.Code != cs3rpc.Code_CODE_OK { | ||
return nil, nil | ||
} | ||
res2, err := client.WhoAmI(ctx, &cs3gateway.WhoAmIRequest{ | ||
Token: res.Token, | ||
}) | ||
if err != nil || res2.Status.Code != cs3rpc.Code_CODE_OK { | ||
return nil, nil | ||
} | ||
|
||
user, err := newCS3User(res2.User) | ||
if err != nil { | ||
return nil, fmt.Errorf("cs3 backend resolve username data error: %v", err) | ||
} | ||
|
||
return user, nil | ||
} | ||
|
||
// RefreshSession implements the Backend interface. | ||
func (b *CS3Backend) RefreshSession(ctx context.Context, userID string, sessionRef *string, claims map[string]interface{}) error { | ||
return nil | ||
} | ||
|
||
// DestroySession implements the Backend interface providing destroy CS3 session. | ||
func (b *CS3Backend) DestroySession(ctx context.Context, sessionRef *string) error { | ||
b.sessions.Remove(*sessionRef) | ||
return nil | ||
} | ||
|
||
// UserClaims implements the Backend interface, providing user specific claims | ||
// for the user specified by the userID. | ||
func (b *CS3Backend) UserClaims(userID string, authorizedScopes map[string]bool) map[string]interface{} { | ||
return nil | ||
// TODO should we return the "ownclouduuid" as a claim? there is also "LibgreGraph.UUID" / lico.ScopeUniqueUserID | ||
} | ||
|
||
// ScopesSupported implements the Backend interface, providing supported scopes | ||
// when running this backend. | ||
func (b *CS3Backend) ScopesSupported() []string { | ||
return b.supportedScopes | ||
} | ||
|
||
// ScopesMeta implements the Backend interface, providing meta data for | ||
// supported scopes. | ||
func (b *CS3Backend) ScopesMeta() *scopes.Scopes { | ||
return nil | ||
} | ||
|
||
// Name implements the Backend interface. | ||
func (b *CS3Backend) Name() string { | ||
return cs3BackendName | ||
} | ||
|
||
func (b *CS3Backend) connect(ctx context.Context) (*grpc.ClientConn, error) { | ||
if b.insecure { | ||
return grpc.Dial(b.gatewayURI, grpc.WithTransportCredentials(ins.NewCredentials())) | ||
} | ||
|
||
creds := credentials.NewTLS(b.tlsConfig) | ||
return grpc.Dial(b.gatewayURI, grpc.WithTransportCredentials(creds)) | ||
} | ||
|
||
func (b *CS3Backend) getSessionForUser(ctx context.Context, userEntryID string, sessionRef *string, register bool, refresh bool, removeIfRegistered bool) (*cs3Session, error) { | ||
if sessionRef == nil { | ||
return nil, nil | ||
} | ||
|
||
var session *cs3Session | ||
if s, ok := b.sessions.Get(*sessionRef); ok { | ||
// Existing session. | ||
session = s.(*cs3Session) | ||
if session != nil { | ||
return session, nil | ||
} | ||
} | ||
|
||
return session, nil | ||
} |
Oops, something went wrong.