Custom User System Integration Guide
1. System Architecture Overview
The project adopts an abstract SSO interface design that supports integration with multiple authentication services. By implementing the unified SSOInterface, you can easily integrate your own custom user system.
Currently, two authentication services are supported:
- Casdoor: An open-source identity and access management platform
- Paraview: A closed-source identity authentication system compliant with national standards
2. Core Interface Definition
All SSO services in the system must implement the SSOInterface, which is defined in builder/rpc/sso.go:
type SSOInterface interface {
UpdateUserInfo(ctx context.Context, userInfo *SSOUpdateUserInfo) error
GetUserInfo(ctx context.Context, accessToken string) (*SSOUserInfo, error)
GetOAuthToken(ctx context.Context, code, state string) (*oauth2.Token, error)
DeleteUser(ctx context.Context, uuid string) error
IsExistByName(ctx context.Context, name string) (bool, error)
IsExistByEmail(ctx context.Context, email string) (bool, error)
IsExistByPhone(ctx context.Context, phone string) (bool, error)
CreateInvitation(ctx context.Context, code string) error
GetInvitationCode(ctx context.Context, userUUID string) (string, error)
}
3. Steps for Custom User System Integration
Step 1: Create a Custom SSO Client Implementation
First, create a struct that implements the SSOInterface. Refer to existing implementations such as casdoorClientImpl or paraviewClientImpl as examples.
package rpc
import (
"context"
"fmt"
)
// Custom SSO client implementation
type customSSOClientImpl struct {
// Custom configuration fields
endpoint string
clientID string
clientSecret string
// other necessary fields
}
var (
_ SSOInterface = (*customSSOClientImpl)(nil)
)
Step 2: Implement Interface Methods
Implement all methods defined in SSOInterface for your custom client:
// Retrieve OAuth token
func (c *customSSOClientImpl) GetOAuthToken(ctx context.Context, code, state string) (*oauth2.Token, error) {
// Implement logic to obtain the token from the custom SSO service
// ...
return token, nil
}
// Retrieve user information
func (c *customSSOClientImpl) GetUserInfo(ctx context.Context, accessToken string) (*SSOUserInfo, error) {
// Implement logic to get user info using access token
// ...
return &SSOUserInfo{
Name: "username",
Email: "email",
UUID: "unique_user_id",
RegProvider: "customSSO",
// populate other user information fields
}, nil
}
// Implement other interface methods...
func (c *customSSOClientImpl) UpdateUserInfo(ctx context.Context, userInfo *SSOUpdateUserInfo) error { /* ... */ }
func (c *customSSOClientImpl) DeleteUser(ctx context.Context, uuid string) error { /* ... */ }
func (c *customSSOClientImpl) IsExistByName(ctx context.Context, name string) (bool, error) { /* ... */ }
func (c *customSSOClientImpl) IsExistByEmail(ctx context.Context, email string) (bool, error) { /* ... */ }
func (c *customSSOClientImpl) IsExistByPhone(ctx context.Context, phone string) (bool, error) { /* ... */ }
func (c *customSSOClientImpl) CreateInvitation(ctx context.Context, code string) error { /* ... */ }
func (c *customSSOClientImpl) GetInvitationCode(ctx context.Context, userUUID string) (string, error) { /* ... */ }
Step 3: Create a Client Constructor
// Create a constructor for the custom SSO client
func NewCustomSSOClient(config *config.Config) SSOInterface {
return &customSSOClientImpl{
endpoint: config.CustomSSO.Endpoint,
clientID: config.CustomSSO.ClientID,
clientSecret: config.CustomSSO.ClientSecret,
}
}
Step 4: Add Custom SSO Configuration Options
// Add in the Config struct
SSOType string `env:"STARHUB_SERVER_SSO_TYPE" default:"casdoor"`
// Add custom SSO configuration
CustomSSO struct {
ClientID string `env:"STARHUB_SERVER_CUSTOM_SSO_CLIENT_ID" default:"client_id"`
ClientSecret string `env:"STARHUB_SERVER_CUSTOM_SSO_CLIENT_SECRET" default:"client_secret"`
Endpoint string `env:"STARHUB_SERVER_CUSTOM_SSO_ENDPOINT" default:"http://localhost:8080"`
// Add other configuration items if necessary
}
Step 5: Modify the SSO Client Factory Function
const (
SSOTypeCasdoor = "casdoor"
SSOTypeParaview = "paraview"
SSOTypeCustom = "custom" // Add custom SSO type
)
func NewSSOClient(config *config.Config) (SSOInterface, error) {
switch config.SSOType {
case SSOTypeCasdoor:
// Existing Casdoor implementation
// ...
case SSOTypeParaview:
// Existing Paraview implementation
// ...
case SSOTypeCustom:
// Add support for custom SSO
return NewCustomSSOClient(config), nil
default:
return nil, fmt.Errorf("invalid sso type: %s", config.SSOType)
}
}
4. Configuration and Environment Variables
# Specify the use of custom SSO
STARHUB_SERVER_SSO_TYPE=custom
# Custom SSO configuration
STARHUB_SERVER_CUSTOM_SSO_CLIENT_ID=your_client_id
STARHUB_SERVER_CUSTOM_SSO_CLIENT_SECRET=your_client_secret
STARHUB_SERVER_CUSTOM_SSO_ENDPOINT=http://your-sso-server.com
5. Example
Here is a simplified example of a custom SSO implementation based on the OAuth 2.0 protocol:
package rpc
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"golang.org/x/oauth2"
"opencsg.com/csghub-server/common/config"
"opencsg.com/csghub-server/common/errorx"
)
// Custom SSO client implementation
type customSSOClientImpl struct {
endpoint string
clientID string
clientSecret string
redirectURI string
}
// Create a new custom SSO client
func NewCustomSSOClient(config *config.Config) SSOInterface {
return &customSSOClientImpl{
endpoint: config.CustomSSO.Endpoint,
clientID: config.CustomSSO.ClientID,
clientSecret: config.CustomSSO.ClientSecret,
redirectURI: config.CustomSSO.RedirectURI,
}
}
// Retrieve OAuth token
func (c *customSSOClientImpl) GetOAuthToken(ctx context.Context, code, state string) (*oauth2.Token, error) {
// Build token request URL
tokenURL := fmt.Sprintf("%s/oauth2/token", c.endpoint)
// Prepare request body
data := strings.NewReader(fmt.Sprintf(
"grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s&client_secret=%s",
code, c.redirectURI, c.clientID, c.clientSecret,
))
// Send request
resp, err := http.Post(tokenURL, "application/x-www-form-urlencoded", data)
if err != nil {
return nil, errorx.RemoteSvcFail(err, errorx.Ctx().Set("service", "custom-sso"))
}
defer resp.Body.Close()
// Handle response
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get token: %s", string(body))
}
// Parse token
var token oauth2.Token
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return nil, err
}
return &token, nil
}
// Retrieve user information
func (c *customSSOClientImpl) GetUserInfo(ctx context.Context, accessToken string) (*SSOUserInfo, error) {
// Build user info request URL
userInfoURL := fmt.Sprintf("%s/api/userinfo", c.endpoint)
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL, nil)
if err != nil {
return nil, err
}
// Set Authorization header
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
// Send request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errorx.RemoteSvcFail(err, errorx.Ctx().Set("service", "custom-sso"))
}
defer resp.Body.Close()
// Handle response
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get user info: %s", string(body))
}
// Parse user info
var userData map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil {
return nil, err
}
// Build SSOUserInfo object
return &SSOUserInfo{
Name: userData["username"].(string),
Email: userData["email"].(string),
UUID: userData["id"].(string),
RegProvider: SSOTypeCustom,
// populate additional fields
}, nil
}
// Implement other interface methods...
// UpdateUserInfo, DeleteUser, IsExistByName, IsExistByEmail, IsExistByPhone,
// CreateInvitation, GetInvitationCode
6. Notes
-
Error Handling: Ensure proper error handling logic and use errorx.RemoteSvcFail to wrap remote service call errors.
-
Security: Protect sensitive information such as client secrets — avoid exposing them in logs.
-
Data Mapping: Verify that user data returned from the custom SSO service correctly maps to the SSOUserInfo struct.
-
Concurrency: Ensure that the implemented methods are thread-safe.
-
Testing: Fully test all interface methods before deploying to a production environment.
By following the steps above, you can successfully integrate your own custom user system into the project, achieving unified user authentication and management.