Replace Fosite OIDC provider with embedded Dex
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 2m26s
All checks were successful
Build and Push Docker Image / build_and_push (push) Successful in 2m26s
This commit is contained in:
151
oidc-provider/connector/connector.go
Normal file
151
oidc-provider/connector/connector.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user