Files
mobility-accounts/oidc-provider/connector/connector.go
Arnaud Delcasse 722c89e86a
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 2m26s
Replace Fosite OIDC provider with embedded Dex
2026-03-02 20:08:06 +01:00

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
}