1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/service/descope_service.go
2026-01-27 15:37:00 +09:00

124 lines
3.5 KiB
Go

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)
}