All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 2m26s
152 lines
4.6 KiB
Go
152 lines
4.6 KiB
Go
package connector
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/dexidp/dex/connector"
|
|
"gitlab.com/mobicoop/solidarity/services/mobility-accounts/handlers"
|
|
"gitlab.com/mobicoop/solidarity/services/mobility-accounts/storage"
|
|
)
|
|
|
|
// Config is the connector configuration used by Dex's ConnectorConfig interface.
|
|
// Fields are injected in-process (not via JSON unmarshaling).
|
|
type Config struct {
|
|
Handler *handlers.MobilityAccountsHandler
|
|
Storage storage.Storage
|
|
Namespace string
|
|
}
|
|
|
|
// Open satisfies server.ConnectorConfig. It returns a Connector that implements
|
|
// connector.PasswordConnector and connector.RefreshConnector.
|
|
func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
|
|
if c.Handler == nil {
|
|
return nil, fmt.Errorf("mobilityaccounts connector: handler is nil")
|
|
}
|
|
return &Connector{
|
|
handler: c.Handler,
|
|
storage: c.Storage,
|
|
namespace: c.Namespace,
|
|
logger: logger,
|
|
}, nil
|
|
}
|
|
|
|
// Connector bridges Dex authentication to the mobility-accounts handler.
|
|
// It implements connector.PasswordConnector and connector.RefreshConnector.
|
|
type Connector struct {
|
|
handler *handlers.MobilityAccountsHandler
|
|
storage storage.Storage
|
|
namespace string
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// Prompt returns the label displayed on the password form input.
|
|
func (c *Connector) Prompt() string { return "Email" }
|
|
|
|
// Login authenticates a user via the mobility-accounts handler and maps the
|
|
// account data to a Dex Identity with standard OIDC claims.
|
|
func (c *Connector) Login(ctx context.Context, s connector.Scopes, username, password string) (connector.Identity, bool, error) {
|
|
account, err := c.handler.Login(username, password, c.namespace)
|
|
if err != nil {
|
|
// Invalid credentials: return validPassword=false, no error
|
|
return connector.Identity{}, false, nil
|
|
}
|
|
|
|
c.logger.Info("connector login", "account_id", account.ID, "data_keys", dataKeys(account.Data), "groups_raw", fmt.Sprintf("%T: %v", account.Data["groups"], account.Data["groups"]))
|
|
|
|
identity := buildIdentity(account)
|
|
c.logger.Info("connector identity", "groups", identity.Groups, "email", identity.Email, "username", identity.Username)
|
|
return identity, true, nil
|
|
}
|
|
|
|
// Refresh is called when a client uses a refresh token. It reloads the account
|
|
// from storage to reflect any changes since the last authentication.
|
|
func (c *Connector) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
|
|
account, err := c.storage.DB.GetAccount(identity.UserID)
|
|
if err != nil {
|
|
return identity, fmt.Errorf("mobilityaccounts connector: refresh failed: %w", err)
|
|
}
|
|
|
|
return buildIdentity(account), nil
|
|
}
|
|
|
|
// buildIdentity maps a mobility-accounts Account to a Dex connector.Identity.
|
|
// Dex maps Identity.Username to the "name" OIDC claim and
|
|
// Identity.PreferredUsername to the "preferred_username" claim.
|
|
func buildIdentity(account *storage.Account) connector.Identity {
|
|
displayName := getStringData(account, "display_name")
|
|
if displayName == "" {
|
|
displayName = derefString(account.Authentication.Local.Username)
|
|
}
|
|
return connector.Identity{
|
|
UserID: account.ID,
|
|
Username: displayName,
|
|
PreferredUsername: derefString(account.Authentication.Local.Username),
|
|
Email: getStringData(account, "email"),
|
|
EmailVerified: true,
|
|
Groups: getGroups(account),
|
|
ConnectorData: nil,
|
|
}
|
|
}
|
|
|
|
// dataKeys returns the keys of a map for debug logging.
|
|
func dataKeys(m map[string]any) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// derefString safely dereferences a *string, returning "" if nil.
|
|
func derefString(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
// getStringData retrieves a string value from account.Data by key.
|
|
func getStringData(account *storage.Account, key string) string {
|
|
if account.Data == nil {
|
|
return ""
|
|
}
|
|
v, ok := account.Data[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return s
|
|
}
|
|
|
|
// getGroups retrieves the groups slice from account.Data["groups"].
|
|
// Uses JSON round-tripping to handle any BSON/primitive type from MongoDB.
|
|
func getGroups(account *storage.Account) []string {
|
|
if account.Data == nil {
|
|
return []string{}
|
|
}
|
|
v, ok := account.Data["groups"]
|
|
if !ok || v == nil {
|
|
return []string{}
|
|
}
|
|
|
|
// JSON round-trip handles primitive.A, []interface{}, []string, etc.
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
var groups []string
|
|
if err := json.Unmarshal(b, &groups); err != nil {
|
|
return []string{}
|
|
}
|
|
if groups == nil {
|
|
return []string{}
|
|
}
|
|
return groups
|
|
}
|