mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
354 lines
10 KiB
Go
354 lines
10 KiB
Go
package handlers
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"net/url"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/labstack/echo/v4"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestSIOPAuthorization tests Self-Issued OpenID Provider authorization
|
||
|
|
func TestSIOPAuthorization(t *testing.T) {
|
||
|
|
e := echo.New()
|
||
|
|
|
||
|
|
t.Run("ValidSIOPRequest", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("response_type", "id_token")
|
||
|
|
q.Set("client_id", "test-client")
|
||
|
|
q.Set("redirect_uri", "http://localhost:3000/callback")
|
||
|
|
q.Set("scope", "openid did")
|
||
|
|
q.Set("nonce", "test-nonce")
|
||
|
|
q.Set("state", "test-state")
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
// Set authenticated user DID
|
||
|
|
c.Set("user_did", "did:sonr:testuser")
|
||
|
|
|
||
|
|
err := HandleSIOPAuthorization(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Should redirect with ID token
|
||
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
||
|
|
location := rec.Header().Get("Location")
|
||
|
|
assert.Contains(t, location, "id_token=")
|
||
|
|
assert.Contains(t, location, "state=test-state")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("InvalidResponseType", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("response_type", "code") // SIOP only supports id_token
|
||
|
|
q.Set("client_id", "test-client")
|
||
|
|
q.Set("redirect_uri", "http://localhost:3000/callback")
|
||
|
|
q.Set("scope", "openid")
|
||
|
|
q.Set("nonce", "test-nonce")
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPAuthorization(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||
|
|
|
||
|
|
var errorResp map[string]string
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "unsupported_response_type", errorResp["error"])
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("MissingScopeOpenID", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("response_type", "id_token")
|
||
|
|
q.Set("client_id", "test-client")
|
||
|
|
q.Set("redirect_uri", "http://localhost:3000/callback")
|
||
|
|
q.Set("scope", "profile email") // Missing 'openid'
|
||
|
|
q.Set("nonce", "test-nonce")
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPAuthorization(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||
|
|
|
||
|
|
var errorResp map[string]string
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "invalid_scope", errorResp["error"])
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("UnauthenticatedUser", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("response_type", "id_token")
|
||
|
|
q.Set("client_id", "test-client")
|
||
|
|
q.Set("redirect_uri", "http://localhost:3000/callback")
|
||
|
|
q.Set("scope", "openid")
|
||
|
|
q.Set("nonce", "test-nonce")
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
// No user_did set - user not authenticated
|
||
|
|
|
||
|
|
err := HandleSIOPAuthorization(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusUnauthorized, rec.Code)
|
||
|
|
|
||
|
|
var errorResp map[string]string
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "authentication_required", errorResp["error"])
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSIOPRegistration tests dynamic client registration for SIOP
|
||
|
|
func TestSIOPRegistration(t *testing.T) {
|
||
|
|
e := echo.New()
|
||
|
|
|
||
|
|
t.Run("ValidRegistration", func(t *testing.T) {
|
||
|
|
registration := map[string]any{
|
||
|
|
"client_name": "Test SIOP Client",
|
||
|
|
"client_purpose": "Testing SIOP functionality",
|
||
|
|
"logo_uri": "https://example.com/logo.png",
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(registration)
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPRegistration(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||
|
|
|
||
|
|
var response map[string]any
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &response)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotEmpty(t, response["client_id"])
|
||
|
|
assert.Equal(t, "Test SIOP Client", response["client_name"])
|
||
|
|
assert.Contains(t, response["subject_syntax_types_supported"], "did")
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("MissingClientName", func(t *testing.T) {
|
||
|
|
registration := map[string]any{
|
||
|
|
"client_purpose": "Testing",
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(registration)
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPRegistration(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||
|
|
|
||
|
|
var errorResp map[string]string
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "invalid_request", errorResp["error"])
|
||
|
|
assert.Contains(t, errorResp["error_description"], "Client name is required")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSIOPMetadata tests SIOP metadata endpoint
|
||
|
|
func TestSIOPMetadata(t *testing.T) {
|
||
|
|
e := echo.New()
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/siop/metadata", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPMetadata(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||
|
|
|
||
|
|
var metadata map[string]any
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &metadata)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
// Verify required SIOP metadata fields
|
||
|
|
assert.Equal(t, "https://self-issued.me/v2", metadata["issuer"])
|
||
|
|
assert.Equal(t, "openid://", metadata["authorization_endpoint"])
|
||
|
|
assert.Contains(t, metadata["response_types_supported"], "id_token")
|
||
|
|
assert.Contains(t, metadata["scopes_supported"], "openid")
|
||
|
|
assert.Contains(t, metadata["subject_types_supported"], "pairwise")
|
||
|
|
assert.NotNil(t, metadata["vp_formats_supported"])
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestValidateSIOPRequest tests SIOP request validation
|
||
|
|
func TestValidateSIOPRequest(t *testing.T) {
|
||
|
|
t.Run("ValidRequest", func(t *testing.T) {
|
||
|
|
requestURI := "openid://?" +
|
||
|
|
"response_type=id_token" +
|
||
|
|
"&client_id=test-client" +
|
||
|
|
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
|
||
|
|
"&scope=openid+did" +
|
||
|
|
"&nonce=test-nonce" +
|
||
|
|
"&state=test-state"
|
||
|
|
|
||
|
|
req, err := ValidateSIOPRequest(requestURI)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, req)
|
||
|
|
assert.Equal(t, "id_token", req.ResponseType)
|
||
|
|
assert.Equal(t, "test-client", req.ClientID)
|
||
|
|
assert.Equal(t, "openid did", req.Scope)
|
||
|
|
assert.Equal(t, "test-nonce", req.Nonce)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("InvalidURI", func(t *testing.T) {
|
||
|
|
req, err := ValidateSIOPRequest("not-a-valid-uri")
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Nil(t, req)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("WithClaims", func(t *testing.T) {
|
||
|
|
claims := map[string]any{
|
||
|
|
"id_token": map[string]any{
|
||
|
|
"email": map[string]any{
|
||
|
|
"essential": true,
|
||
|
|
},
|
||
|
|
"name": nil,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
claimsJSON, _ := json.Marshal(claims)
|
||
|
|
|
||
|
|
requestURI := "openid://?" +
|
||
|
|
"response_type=id_token" +
|
||
|
|
"&client_id=test-client" +
|
||
|
|
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
|
||
|
|
"&scope=openid" +
|
||
|
|
"&nonce=test-nonce" +
|
||
|
|
"&claims=" + url.QueryEscape(string(claimsJSON))
|
||
|
|
|
||
|
|
req, err := ValidateSIOPRequest(requestURI)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, req)
|
||
|
|
assert.NotNil(t, req.Claims.IDToken)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSIOPWithVPToken tests SIOP with Verifiable Presentation
|
||
|
|
func TestSIOPWithVPToken(t *testing.T) {
|
||
|
|
e := echo.New()
|
||
|
|
|
||
|
|
vpClaims := map[string]any{
|
||
|
|
"vp_token": map[string]any{
|
||
|
|
"presentation_definition": map[string]any{
|
||
|
|
"id": "test-presentation",
|
||
|
|
"input_descriptors": []map[string]any{
|
||
|
|
{
|
||
|
|
"id": "id_credential",
|
||
|
|
"constraints": map[string]any{
|
||
|
|
"fields": []map[string]any{
|
||
|
|
{
|
||
|
|
"path": []string{"$.type"},
|
||
|
|
"filter": map[string]any{
|
||
|
|
"type": "string",
|
||
|
|
"const": "IdentityCredential",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
claimsJSON, _ := json.Marshal(vpClaims)
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("response_type", "id_token")
|
||
|
|
q.Set("client_id", "test-client")
|
||
|
|
q.Set("redirect_uri", "http://localhost:3000/callback")
|
||
|
|
q.Set("scope", "openid")
|
||
|
|
q.Set("nonce", "test-nonce")
|
||
|
|
q.Set("claims", string(claimsJSON))
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
c.Set("user_did", "did:sonr:testuser")
|
||
|
|
|
||
|
|
// Parse request to set claims
|
||
|
|
siopReq := &SIOPRequest{
|
||
|
|
ResponseType: "id_token",
|
||
|
|
ClientID: "test-client",
|
||
|
|
RedirectURI: "http://localhost:3000/callback",
|
||
|
|
Scope: "openid",
|
||
|
|
Nonce: "test-nonce",
|
||
|
|
}
|
||
|
|
err := json.Unmarshal(claimsJSON, &siopReq.Claims)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
c.Set("siop_request", siopReq)
|
||
|
|
|
||
|
|
err = HandleSIOPAuthorization(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
||
|
|
|
||
|
|
location := rec.Header().Get("Location")
|
||
|
|
assert.Contains(t, location, "id_token=")
|
||
|
|
// VP token generation is optional, so we don't assert its presence
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestHandleSIOPRequest tests complete SIOP request flow
|
||
|
|
func TestHandleSIOPRequest(t *testing.T) {
|
||
|
|
e := echo.New()
|
||
|
|
|
||
|
|
t.Run("ValidRequestURI", func(t *testing.T) {
|
||
|
|
requestURI := "openid://?" +
|
||
|
|
"response_type=id_token" +
|
||
|
|
"&client_id=test-client" +
|
||
|
|
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
|
||
|
|
"&scope=openid" +
|
||
|
|
"&nonce=test-nonce"
|
||
|
|
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
|
||
|
|
q := req.URL.Query()
|
||
|
|
q.Set("request_uri", requestURI)
|
||
|
|
req.URL.RawQuery = q.Encode()
|
||
|
|
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
c.Set("user_did", "did:sonr:testuser")
|
||
|
|
|
||
|
|
err := HandleSIOPRequest(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
// Should process as authorization request
|
||
|
|
assert.Equal(t, http.StatusFound, rec.Code)
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("MissingRequestURI", func(t *testing.T) {
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
c := e.NewContext(req, rec)
|
||
|
|
|
||
|
|
err := HandleSIOPRequest(c)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||
|
|
|
||
|
|
var errorResp map[string]string
|
||
|
|
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, "invalid_request", errorResp["error"])
|
||
|
|
assert.Contains(t, errorResp["error_description"], "Missing request_uri")
|
||
|
|
})
|
||
|
|
}
|