package service import ( "baron-sso-backend/internal/domain" "context" "fmt" "log/slog" "net/http" "os" "time" "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/client" ) type DescopeProvider struct { Client *client.DescopeClient FrontendURL string fieldMapping map[string]string // Key: Broker Field Name, Value: Descope Attribute Key } func NewDescopeProvider(projectID, managementKey string) *DescopeProvider { var descopeClient *client.DescopeClient var err error if projectID != "" { descopeClient, err = client.NewWithConfig(&client.Config{ ProjectID: projectID, ManagementKey: managementKey, }) if err != nil { slog.Warn("Failed to initialize Descope Client in Provider", "error", err) } } // Define the mapping between BrokerUser fields and Descope attributes. // In a real scenario, this could be loaded from a config file. // For this implementation, we hardcode the support to demonstrate the validation. // We map the Broker's required custom attributes to Descope's keys. mapping := map[string]string{ "grade": "customAttributes.userRank", // Broker 'grade' maps to Descope 'userRank' "department": "customAttributes.dept", // Broker 'department' maps to Descope 'dept' } return &DescopeProvider{ Client: descopeClient, FrontendURL: os.Getenv("FRONTEND_URL"), fieldMapping: mapping, } } func (d *DescopeProvider) Name() string { return "Descope" } // GetMetadata returns the schema support information. // Currently, it returns the standard fields Descope supports + the mapped custom attributes. func (d *DescopeProvider) GetMetadata() (*domain.IDPMetadata, error) { // 1. Standard Fields supported by Descope supported := []string{"id", "email", "name", "phone_number"} // 2. Add mapped custom attributes // The Validator checks if the Broker's required keys (e.g., "grade") are present in this list. for brokerKey := range d.fieldMapping { supported = append(supported, brokerKey) } return &domain.IDPMetadata{ SupportedFields: supported, }, nil } func (d *DescopeProvider) InitiatePasswordReset(loginID, redirectUrl string) error { ctx := context.Background() err := d.Client.Auth.Password().SendPasswordReset(ctx, loginID, redirectUrl, nil) if err != nil { slog.Error("Descope SendPasswordReset failed (raw)", "loginID", loginID, "redirectUrl", redirectUrl, "err", err, "err_type", fmt.Sprintf("%T", err), ) if de, ok := err.(*descope.Error); ok { status := de.Info[descope.ErrorInfoKeys.HTTPResponseStatusCode] // "Status-Code" slog.Error("Descope error details", "code", de.Code, "description", de.Description, "message", de.Message, "status_code", status, "info", de.Info, ) } } return err } func (d *DescopeProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) { ctx := context.Background() authInfo, err := d.Client.Auth.MagicLink().Verify(ctx, token, nil) if err != nil { return nil, err } res := &domain.AuthInfo{ SessionToken: &domain.Token{ JWT: authInfo.SessionToken.JWT, Expiration: time.Unix(authInfo.SessionToken.Expiration, 0), }, } if authInfo.RefreshToken != nil { res.RefreshToken = &domain.Token{ JWT: authInfo.RefreshToken.JWT, Expiration: time.Unix(authInfo.RefreshToken.Expiration, 0), } } return res, nil } func (d *DescopeProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error { ctx := context.Background() return d.Client.Auth.Password().UpdateUserPassword(ctx, loginID, newPassword, r) }