forked from baron/baron-sso
- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping) - Implemented idempotency and visibility restoration for soft-deleted users - Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields - Added logic to reclaim identifiers (login_id) from colliding records - Added frontend E2E and backend unit tests for UUID integrity and conflict handling - Fixed i18n, formatting, and mock tests to satisfy code-check - Applied 'go fix' for 'omitzero' tags and general Go standards
640 lines
19 KiB
Go
640 lines
19 KiB
Go
package service
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"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
|
|
}
|
|
|
|
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) ([]domain.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 []domain.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) (*domain.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 domain.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) (*domain.HydraClient, error) {
|
|
// JSON Patch format
|
|
payload := []map[string]any{
|
|
{
|
|
"op": "replace",
|
|
"path": "/metadata/status",
|
|
"value": 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/json-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 domain.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) CreateClient(ctx context.Context, client domain.HydraClient) (*domain.HydraClient, error) {
|
|
body, _ := json.Marshal(client)
|
|
endpoint := fmt.Sprintf("%s/clients", strings.TrimRight(s.AdminURL, "/"))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("hydra admin: create client failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var created domain.HydraClient
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode created client failed: %w", err)
|
|
}
|
|
return &created, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) UpdateClient(ctx context.Context, clientID string, client domain.HydraClient) (*domain.HydraClient, error) {
|
|
client.ClientID = clientID
|
|
body, _ := json.Marshal(client)
|
|
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/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: update client failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var updated domain.HydraClient
|
|
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode updated client failed: %w", err)
|
|
}
|
|
return &updated, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) DeleteClient(ctx context.Context, clientID string) error {
|
|
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
|
|
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 == http.StatusNotFound {
|
|
return ErrHydraNotFound
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return fmt.Errorf("hydra admin: delete client failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]domain.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 == http.StatusNoContent {
|
|
return []domain.HydraConsentSession{}, nil
|
|
}
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
|
if resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("hydra admin: list consent sessions failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
if len(body) == 0 {
|
|
return []domain.HydraConsentSession{}, nil
|
|
}
|
|
|
|
var sessions []domain.HydraConsentSession
|
|
if err := json.Unmarshal(body, &sessions); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode consent sessions failed: %w body=%s", err, string(body))
|
|
}
|
|
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
|
|
} else {
|
|
params["all"] = "true"
|
|
}
|
|
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
|
|
}
|
|
|
|
type AcceptLoginRequestResponse struct {
|
|
RedirectTo string `json:"redirectTo"`
|
|
}
|
|
|
|
type AcceptConsentRequestResponse struct {
|
|
RedirectTo string `json:"redirectTo"`
|
|
}
|
|
|
|
type RejectConsentRequestResponse struct {
|
|
RedirectTo string `json:"redirectTo"`
|
|
}
|
|
|
|
type RejectLoginRequestResponse struct {
|
|
RedirectTo string `json:"redirectTo"`
|
|
}
|
|
|
|
func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge string) (*domain.HydraConsentRequest, error) {
|
|
params := map[string]string{
|
|
"consent_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for get consent failed: %w", err)
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: get consent request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: get consent failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var consentReq domain.HydraConsentRequest
|
|
if err := json.Unmarshal(body, &consentReq); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode get consent response failed: %w", err)
|
|
}
|
|
|
|
return &consentReq, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) RejectConsentRequest(ctx context.Context, challenge string) (*RejectConsentRequestResponse, error) {
|
|
params := map[string]string{
|
|
"consent_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/reject", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"error": "access_denied",
|
|
"error_description": "The user decided to reject the consent request.",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for reject consent failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: reject consent request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: reject consent failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var hydraResp struct {
|
|
RedirectTo string `json:"redirect_to"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &hydraResp); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode reject consent response failed: %w", err)
|
|
}
|
|
|
|
return &RejectConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) RejectLoginRequest(ctx context.Context, challenge, error, errorDescription string) (*RejectLoginRequestResponse, error) {
|
|
params := map[string]string{
|
|
"login_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login/reject", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"error": error,
|
|
"error_description": errorDescription,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for reject login failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: reject login request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: reject login failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var hydraResp struct {
|
|
RedirectTo string `json:"redirect_to"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &hydraResp); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode reject login response failed: %w", err)
|
|
}
|
|
|
|
return &RejectLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) GetLoginRequest(ctx context.Context, challenge string) (*domain.HydraLoginRequest, error) {
|
|
params := map[string]string{
|
|
"login_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for get login failed: %w", err)
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: get login request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: get login failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var loginReq domain.HydraLoginRequest
|
|
if err := json.Unmarshal(body, &loginReq); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode get login response failed: %w", err)
|
|
}
|
|
|
|
return &loginReq, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge string, grantInfo *domain.HydraConsentRequest, sessionClaims map[string]any) (*AcceptConsentRequestResponse, error) {
|
|
params := map[string]string{
|
|
"consent_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/accept", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"grant_scope": grantInfo.RequestedScope,
|
|
"grant_audience": grantInfo.RequestedAudience,
|
|
"remember": true,
|
|
"remember_for": 2592000,
|
|
}
|
|
if len(sessionClaims) > 0 {
|
|
payload["session"] = map[string]any{
|
|
"id_token": sessionClaims,
|
|
"access_token": sessionClaims,
|
|
}
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for accept consent failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: accept consent request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: accept consent failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
// Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환
|
|
var hydraResp struct {
|
|
RedirectTo string `json:"redirect_to"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &hydraResp); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode accept consent response failed: %w", err)
|
|
}
|
|
|
|
return &AcceptConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
|
|
}
|
|
|
|
func (s *HydraAdminService) AcceptLoginRequest(ctx context.Context, challenge string, subject string) (*AcceptLoginRequestResponse, error) {
|
|
params := map[string]string{
|
|
"login_challenge": challenge,
|
|
}
|
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login/accept", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"subject": subject,
|
|
"remember": true,
|
|
"remember_for": 2592000,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: create request for accept login failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hydra admin: accept login request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("hydra admin: accept login failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
// Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환
|
|
var hydraResp struct {
|
|
RedirectTo string `json:"redirect_to"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &hydraResp); err != nil {
|
|
return nil, fmt.Errorf("hydra admin: decode accept login response failed: %w", err)
|
|
}
|
|
|
|
return &AcceptLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
|
|
}
|
|
|
|
type HydraIntrospectionResponse struct {
|
|
Active bool `json:"active"`
|
|
Subject string `json:"sub"`
|
|
ClientID string `json:"client_id"`
|
|
Scope string `json:"scope"`
|
|
ExpiresAt int64 `json:"exp"`
|
|
IssuedAt int64 `json:"iat"`
|
|
Ext map[string]any `json:"ext"`
|
|
}
|
|
|
|
func (s *HydraAdminService) IntrospectToken(ctx context.Context, token string) (*HydraIntrospectionResponse, error) {
|
|
endpoint := fmt.Sprintf("%s/oauth2/introspect", strings.TrimRight(s.AdminURL, "/"))
|
|
form := url.Values{}
|
|
form.Set("token", token)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
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: introspection failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var res HydraIntrospectionResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
|
return nil, err
|
|
}
|
|
return &res, nil
|
|
}
|