forked from baron/baron-sso
266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrHydraNotFound = errors.New("hydra admin: resource not found")
|
|
|
|
// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
|
|
type HydraAdminService struct {
|
|
AdminURL string
|
|
PublicURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
type HydraClient struct {
|
|
ClientID string `json:"client_id"`
|
|
ClientName string `json:"client_name,omitempty"`
|
|
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
|
GrantTypes []string `json:"grant_types,omitempty"`
|
|
ResponseTypes []string `json:"response_types,omitempty"`
|
|
Scope string `json:"scope,omitempty"`
|
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type HydraConsentSession struct {
|
|
Subject string `json:"subject"`
|
|
GrantedScope []string `json:"granted_scope"`
|
|
GrantedAudience []string `json:"granted_audience,omitempty"`
|
|
Remember bool `json:"remember"`
|
|
AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
|
|
RequestedAt *time.Time `json:"requested_at,omitempty"`
|
|
Client HydraClient `json:"client"`
|
|
}
|
|
|
|
func NewHydraAdminService() *HydraAdminService {
|
|
return &HydraAdminService{
|
|
AdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
|
|
PublicURL: getenv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
|
|
}
|
|
}
|
|
|
|
func (s *HydraAdminService) ListClients(ctx context.Context, limit, offset int) ([]HydraClient, error) {
|
|
endpoint, err := s.buildURL("/clients", map[string]int{
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, ErrHydraNotFound
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("hydra admin: list clients failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var clients []HydraClient
|
|
if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode clients failed: %w", err)
|
|
}
|
|
return clients, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) GetClient(ctx context.Context, clientID string) (*HydraClient, error) {
|
|
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, ErrHydraNotFound
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("hydra admin: get client failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var client HydraClient
|
|
if err := json.NewDecoder(resp.Body).Decode(&client); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode client failed: %w", err)
|
|
}
|
|
return &client, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, status string) (*HydraClient, error) {
|
|
payload := map[string]interface{}{
|
|
"metadata": map[string]interface{}{
|
|
"status": status,
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/merge-patch+json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, ErrHydraNotFound
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("hydra admin: patch client failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var updated HydraClient
|
|
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode patched client failed: %w", err)
|
|
}
|
|
return &updated, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]HydraConsentSession, error) {
|
|
params := map[string]string{
|
|
"subject": subject,
|
|
}
|
|
if clientID != "" {
|
|
params["client"] = clientID
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("hydra admin: list consent sessions failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var sessions []HydraConsentSession
|
|
if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode consent sessions failed: %w", err)
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject, clientID string) error {
|
|
params := map[string]string{
|
|
"subject": subject,
|
|
}
|
|
if clientID != "" {
|
|
params["client"] = clientID
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return fmt.Errorf("hydra admin: revoke consent failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *HydraAdminService) httpClient() *http.Client {
|
|
if s.HTTPClient != nil {
|
|
return s.HTTPClient
|
|
}
|
|
return &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: &http.Transport{
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 5 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}).DialContext,
|
|
TLSHandshakeTimeout: 5 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *HydraAdminService) buildURL(path string, ints map[string]int) (string, error) {
|
|
base := strings.TrimRight(s.AdminURL, "/")
|
|
u, err := url.Parse(base + path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
q := u.Query()
|
|
for key, value := range ints {
|
|
if value > 0 {
|
|
q.Set(key, strconv.Itoa(value))
|
|
}
|
|
}
|
|
u.RawQuery = q.Encode()
|
|
return u.String(), nil
|
|
}
|
|
|
|
func (s *HydraAdminService) buildURLWithParams(path string, params map[string]string) (string, error) {
|
|
base := strings.TrimRight(s.AdminURL, "/")
|
|
u, err := url.Parse(base + path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
q := u.Query()
|
|
for key, value := range params {
|
|
if value != "" {
|
|
q.Set(key, value)
|
|
}
|
|
}
|
|
u.RawQuery = q.Encode()
|
|
return u.String(), nil
|
|
}
|