forked from baron/baron-sso
linked RP 응답에 1st-party 앱 자동 로그인 init_url 추가
This commit is contained in:
@@ -4483,7 +4483,8 @@ type linkedRpSummary struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Logo string `json:"logo,omitempty"`
|
Logo string `json:"logo,omitempty"`
|
||||||
URL string `json:"url,omitempty"` // Added
|
URL string `json:"url,omitempty"`
|
||||||
|
InitURL string `json:"init_url,omitempty"`
|
||||||
LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"`
|
LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Scopes []string `json:"scopes,omitempty"`
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
@@ -4564,17 +4565,19 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
|||||||
if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" {
|
if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" {
|
||||||
scopes = strings.Fields(client.Scope)
|
scopes = strings.Fields(client.Scope)
|
||||||
}
|
}
|
||||||
|
initURL := resolveLinkedRPInitURL(client.ClientID, scopes, client.RedirectURIs)
|
||||||
|
|
||||||
existing := records[clientID]
|
existing := records[clientID]
|
||||||
if existing == nil {
|
if existing == nil {
|
||||||
records[clientID] = &linkedRpRecord{
|
records[clientID] = &linkedRpRecord{
|
||||||
linkedRpSummary: linkedRpSummary{
|
linkedRpSummary: linkedRpSummary{
|
||||||
ID: clientID,
|
ID: clientID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Logo: extractHydraClientLogo(client.Metadata),
|
Logo: extractHydraClientLogo(client.Metadata),
|
||||||
URL: clientURL,
|
URL: clientURL,
|
||||||
Status: "active", // Hydra 세션이 있으면 활성
|
InitURL: initURL,
|
||||||
Scopes: scopes,
|
Status: "active", // Hydra 세션이 있으면 활성
|
||||||
|
Scopes: scopes,
|
||||||
},
|
},
|
||||||
lastAuth: lastAuth,
|
lastAuth: lastAuth,
|
||||||
}
|
}
|
||||||
@@ -4590,6 +4593,9 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
|||||||
if existing.URL == "" {
|
if existing.URL == "" {
|
||||||
existing.URL = clientURL
|
existing.URL = clientURL
|
||||||
}
|
}
|
||||||
|
if existing.InitURL == "" {
|
||||||
|
existing.InitURL = initURL
|
||||||
|
}
|
||||||
existing.Scopes = mergeScopes(existing.Scopes, scopes)
|
existing.Scopes = mergeScopes(existing.Scopes, scopes)
|
||||||
if lastAuth.After(existing.lastAuth) {
|
if lastAuth.After(existing.lastAuth) {
|
||||||
existing.lastAuth = lastAuth
|
existing.lastAuth = lastAuth
|
||||||
@@ -4644,15 +4650,21 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
|||||||
client.ClientURI,
|
client.ClientURI,
|
||||||
client.RedirectURIs,
|
client.RedirectURIs,
|
||||||
)
|
)
|
||||||
|
initURL := resolveLinkedRPInitURL(
|
||||||
|
client.ClientID,
|
||||||
|
dc.GrantedScopes,
|
||||||
|
client.RedirectURIs,
|
||||||
|
)
|
||||||
|
|
||||||
records[dc.ClientID] = &linkedRpRecord{
|
records[dc.ClientID] = &linkedRpRecord{
|
||||||
linkedRpSummary: linkedRpSummary{
|
linkedRpSummary: linkedRpSummary{
|
||||||
ID: dc.ClientID,
|
ID: dc.ClientID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Logo: extractHydraClientLogo(client.Metadata),
|
Logo: extractHydraClientLogo(client.Metadata),
|
||||||
URL: clientURL,
|
URL: clientURL,
|
||||||
Status: status,
|
InitURL: initURL,
|
||||||
Scopes: dc.GrantedScopes,
|
Status: status,
|
||||||
|
Scopes: dc.GrantedScopes,
|
||||||
},
|
},
|
||||||
lastAuth: dc.UpdatedAt,
|
lastAuth: dc.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -4726,6 +4738,11 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
record.URL = clientURL
|
record.URL = clientURL
|
||||||
|
record.InitURL = resolveLinkedRPInitURL(
|
||||||
|
client.ClientID,
|
||||||
|
scopes,
|
||||||
|
client.RedirectURIs,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Hydra 정보 없음 (삭제됨 등) -> Audit 정보나 ID로 대체
|
// Hydra 정보 없음 (삭제됨 등) -> Audit 정보나 ID로 대체
|
||||||
if record.Name == "" {
|
if record.Name == "" {
|
||||||
@@ -6778,6 +6795,63 @@ func resolveLinkedRPURL(clientID string, clientURI string, redirectURIs []string
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveLinkedRPInitURL(clientID string, scopes []string, redirectURIs []string) string {
|
||||||
|
clientID = strings.TrimSpace(clientID)
|
||||||
|
if clientID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch clientID {
|
||||||
|
case "adminfront":
|
||||||
|
if value := strings.TrimRight(strings.TrimSpace(os.Getenv("ADMINFRONT_URL")), "/"); value != "" {
|
||||||
|
return value + "/login?auto=1"
|
||||||
|
}
|
||||||
|
case "devfront":
|
||||||
|
if value := strings.TrimRight(strings.TrimSpace(os.Getenv("DEVFRONT_URL")), "/"); value != "" {
|
||||||
|
return value + "/login?auto=1&returnTo=%2Fclients"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydraPublicURL := strings.TrimRight(os.Getenv("HYDRA_PUBLIC_URL"), "/")
|
||||||
|
if hydraPublicURL == "" {
|
||||||
|
userfrontURL := strings.TrimRight(os.Getenv("USERFRONT_URL"), "/")
|
||||||
|
if userfrontURL == "" {
|
||||||
|
userfrontURL = "https://sso.hmac.kr"
|
||||||
|
}
|
||||||
|
hydraPublicURL = userfrontURL + "/oidc"
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURI := ""
|
||||||
|
if len(redirectURIs) > 0 {
|
||||||
|
redirectURI = strings.TrimSpace(redirectURIs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedScopes := make([]string, 0, len(scopes)+1)
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, scope := range append([]string{"openid"}, scopes...) {
|
||||||
|
scope = strings.TrimSpace(scope)
|
||||||
|
if scope == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[scope]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[scope] = struct{}{}
|
||||||
|
mergedScopes = append(mergedScopes, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("client_id", clientID)
|
||||||
|
params.Set("response_type", "code")
|
||||||
|
params.Set("scope", strings.Join(mergedScopes, " "))
|
||||||
|
params.Set("state", GenerateSecureAlnumToken(16))
|
||||||
|
if redirectURI != "" {
|
||||||
|
params.Set("redirect_uri", redirectURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/oauth2/auth?%s", hydraPublicURL, params.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
func mergeScopes(current []string, next []string) []string {
|
func mergeScopes(current []string, next []string) []string {
|
||||||
if len(next) == 0 {
|
if len(next) == 0 {
|
||||||
return current
|
return current
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -45,11 +46,14 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
|||||||
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
|
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"client": map[string]interface{}{
|
"client": map[string]interface{}{
|
||||||
"client_id": "client-active",
|
"client_id": "devfront",
|
||||||
"client_name": "Active App",
|
"client_name": "DevFront",
|
||||||
|
"redirect_uris": []string{
|
||||||
|
"https://active.example.com/callback",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"granted_scope": []string{"openid"},
|
"grant_scope": []string{"openid", "profile"},
|
||||||
"handled_at": time.Now().Format(time.RFC3339),
|
"handled_at": time.Now().Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,8 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
|||||||
|
|
||||||
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
||||||
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
|
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
|
||||||
|
t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc")
|
||||||
|
t.Setenv("DEVFRONT_URL", "http://localhost:5174")
|
||||||
|
|
||||||
app := newLinkedRpTestApp(h)
|
app := newLinkedRpTestApp(h)
|
||||||
|
|
||||||
@@ -123,10 +129,11 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
|||||||
|
|
||||||
var res struct {
|
var res struct {
|
||||||
Items []struct {
|
Items []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
|
InitURL string `json:"init_url"`
|
||||||
} `json:"items"`
|
} `json:"items"`
|
||||||
}
|
}
|
||||||
json.NewDecoder(resp.Body).Decode(&res)
|
json.NewDecoder(resp.Body).Decode(&res)
|
||||||
@@ -138,7 +145,23 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
|||||||
statusMap[item.ID] = item.Status
|
statusMap[item.ID] = item.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "active", statusMap["client-active"])
|
assert.Equal(t, "active", statusMap["devfront"])
|
||||||
assert.Equal(t, "inactive", statusMap["client-consent"])
|
assert.Equal(t, "inactive", statusMap["client-consent"])
|
||||||
assert.Equal(t, "inactive", statusMap["client-audit"])
|
assert.Equal(t, "inactive", statusMap["client-audit"])
|
||||||
|
|
||||||
|
var activeInitURL string
|
||||||
|
for _, item := range res.Items {
|
||||||
|
if item.ID == "devfront" {
|
||||||
|
activeInitURL = item.InitURL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedInitURL, err := url.Parse(activeInitURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "http", parsedInitURL.Scheme)
|
||||||
|
assert.Equal(t, "localhost:5174", parsedInitURL.Host)
|
||||||
|
assert.Equal(t, "/login", parsedInitURL.Path)
|
||||||
|
assert.Equal(t, "1", parsedInitURL.Query().Get("auto"))
|
||||||
|
assert.Equal(t, "/clients", parsedInitURL.Query().Get("returnTo"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user