mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,353 @@
|
||||
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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user