diff --git a/openapi/hello.go b/openapi/hello.go index f3bd5b7a..b267587a 100644 --- a/openapi/hello.go +++ b/openapi/hello.go @@ -15,12 +15,31 @@ func (openapi *OpenAPI) attachHelloWorld(base *gin.RouterGroup) { hello := base.Group("/helloworld") // Health check - hello.GET("/hello", openapi.helloWorldHello) - hello.POST("/hello", openapi.helloWorldHello) + hello.GET("/public", openapi.helloWorldPublic) + hello.POST("/public", openapi.helloWorldPublic) + + // OAuth Protected Resource + hello.GET("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected) + hello.POST("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected) } -// helloWorldHello is the handler for the hello world endpoint -func (openapi *OpenAPI) helloWorldHello(c *gin.Context) { +// helloWorldPublic is the handler for the hello world endpoint +func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) { + serverTime := time.Now().Format(time.RFC3339) + c.JSON(http.StatusOK, gin.H{ + "MESSAGE": "HELLO, WORLD", + "SERVER_TIME": serverTime, + "VERSION": share.VERSION, + "PRVERSION": share.PRVERSION, + "CUI": share.CUI, + "PRCUI": share.PRCUI, + "APP": share.App.Name, + "APP_VERSION": share.App.Version, + }) +} + +// helloWorldHello is the handler for the hello world endpoint +func (openapi *OpenAPI) helloWorldProtected(c *gin.Context) { serverTime := time.Now().Format(time.RFC3339) c.JSON(http.StatusOK, gin.H{ "MESSAGE": "HELLO, WORLD", diff --git a/openapi/hello_test.go b/openapi/hello_test.go index ab7783c8..11ff2074 100644 --- a/openapi/hello_test.go +++ b/openapi/hello_test.go @@ -9,7 +9,7 @@ import ( "github.com/yaoapp/yao/share" ) -func TestHelloWorldHello(t *testing.T) { +func TestHelloWorldPublic(t *testing.T) { serverURL := Prepare(t) defer Clean() @@ -25,14 +25,14 @@ func TestHelloWorldHello(t *testing.T) { path string }{ { - name: "GET hello endpoint", + name: "GET public endpoint", method: "GET", - path: baseURL + "/helloworld/hello", + path: baseURL + "/helloworld/public", }, { - name: "POST hello endpoint", + name: "POST public endpoint", method: "POST", - path: baseURL + "/helloworld/hello", + path: baseURL + "/helloworld/public", }, } @@ -77,3 +77,146 @@ func TestHelloWorldHello(t *testing.T) { }) } } + +func TestHelloWorldProtected(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + // Get base URL from server config + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + // Register a test client for authentication + client := RegisterTestClient(t, "Hello World Protected Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + + // Obtain access token for authentication + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + tests := []struct { + name string + method string + path string + }{ + { + name: "GET protected endpoint with valid token", + method: "GET", + path: baseURL + "/helloworld/protected", + }, + { + name: "POST protected endpoint with valid token", + method: "POST", + path: baseURL + "/helloworld/protected", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create HTTP request with Bearer token + var req *http.Request + var err error + + if tt.method == "GET" { + req, err = http.NewRequest("GET", serverURL+tt.path, nil) + } else { + req, err = http.NewRequest("POST", serverURL+tt.path, nil) + req.Header.Set("Content-Type", "application/json") + } + assert.NoError(t, err) + + // Add Bearer token for authentication + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + // Make HTTP request + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Check status code + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Parse JSON response + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + // Verify response structure and content (same as public endpoint) + assert.Equal(t, "HELLO, WORLD", response["MESSAGE"]) + assert.NotEmpty(t, response["SERVER_TIME"]) + assert.Equal(t, share.VERSION, response["VERSION"]) + assert.Equal(t, share.PRVERSION, response["PRVERSION"]) + assert.Equal(t, share.CUI, response["CUI"]) + assert.Equal(t, share.PRCUI, response["PRCUI"]) + assert.Equal(t, share.App.Name, response["APP"]) + assert.Equal(t, share.App.Version, response["APP_VERSION"]) + + // Check that SERVER_TIME is a valid timestamp format + serverTime, ok := response["SERVER_TIME"].(string) + assert.True(t, ok) + assert.NotEmpty(t, serverTime) + + t.Logf("Protected endpoint accessed successfully with token: %s", tokenInfo.AccessToken[:20]+"...") + }) + } +} + +func TestHelloWorldProtectedUnauthorized(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + // Get base URL from server config + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + tests := []struct { + name string + method string + path string + description string + }{ + { + name: "GET protected endpoint without token", + method: "GET", + path: baseURL + "/helloworld/protected", + description: "No Authorization header", + }, + { + name: "POST protected endpoint without token", + method: "POST", + path: baseURL + "/helloworld/protected", + description: "No Authorization header", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create HTTP request without Authorization header + var req *http.Request + var err error + + if tt.method == "GET" { + req, err = http.NewRequest("GET", serverURL+tt.path, nil) + } else { + req, err = http.NewRequest("POST", serverURL+tt.path, nil) + req.Header.Set("Content-Type", "application/json") + } + assert.NoError(t, err) + + // Make HTTP request (no Authorization header) + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Should return 401 Unauthorized for protected endpoint without token + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + t.Logf("Protected endpoint correctly rejected unauthorized request: %s", tt.description) + }) + } +} diff --git a/openapi/oauth/core.go b/openapi/oauth/core.go index f2bb6dc3..c6925425 100644 --- a/openapi/oauth/core.go +++ b/openapi/oauth/core.go @@ -71,6 +71,11 @@ func (s *Service) Authorize(ctx context.Context, request *types.AuthorizationReq } // Validate scope if provided + // TODO: + // 1. Should validate scope, if not provide, use the default scope + // 2. If scope has "openid", should be redirect to the login page/mobile app authentication + // 3. If scope not has "openid", can't visit the userinfo endpoint + // 4. Security check if request.Scope != "" { scopes := strings.Fields(request.Scope) scopeValidation, err := s.clientProvider.ValidateScope(ctx, request.ClientID, scopes) diff --git a/openapi/oauth/guard.go b/openapi/oauth/guard.go index c90af7b3..027df9d1 100644 --- a/openapi/oauth/guard.go +++ b/openapi/oauth/guard.go @@ -1 +1,22 @@ package oauth + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Guard is the OAuth guard middleware +func (s *Service) Guard(c *gin.Context) { + // Get the token from the request + token := c.GetHeader("Authorization") + + // Validate the token + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + // Validate the token +} diff --git a/openapi/oauth/types/interfaces.go b/openapi/oauth/types/interfaces.go index c95be61a..c53bd7b3 100644 --- a/openapi/oauth/types/interfaces.go +++ b/openapi/oauth/types/interfaces.go @@ -3,6 +3,8 @@ package types import ( "context" "time" + + "github.com/gin-gonic/gin" ) // OAuth interface defines the complete OAuth 2.1 and MCP authorization server functionality @@ -134,6 +136,9 @@ type OAuth interface { // ValidateTokenBinding validates token binding information // This ensures tokens are bound to the correct client or device ValidateTokenBinding(ctx context.Context, token string, binding *TokenBinding) (*ValidationResult, error) + + // Guard is the OAuth guard middleware + Guard(c *gin.Context) } // UserProvider interface for user information retrieval