diff --git a/api/auth/validate_oauth.go b/api/auth/validate_oauth.go new file mode 100644 index 000000000..a941050ee --- /dev/null +++ b/api/auth/validate_oauth.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" +) + +// swagger:operation GET /validate-oauth authenticate ValidateOAuthToken +// +// Validate that a user oauth token was created by Vela +// +// --- +// produces: +// - application/json +// parameters: +// - in: header +// name: Token +// type: string +// required: true +// description: > +// OAuth integration user access token +// responses: +// '200': +// description: Successfully validated +// schema: +// "$ref": "#/definitions/Token" +// '401': +// description: Unable to validate +// schema: +// "$ref": "#/definitions/Error" + +// ValidateOAuthToken represents the API handler to +// validate that a user oauth token was created by Vela. +func ValidateOAuthToken(c *gin.Context) { + // capture middleware values + ctx := c.Request.Context() + + token := c.Request.Header.Get("Token") + if len(token) == 0 { + retErr := fmt.Errorf("unable to validate oauth token: no token provided in header") + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // attempt to validate access token from source OAuth app + ok, err := scm.FromContext(c).ValidateOAuthToken(ctx, token) + if err != nil { + retErr := fmt.Errorf("unable to validate oauth token: %w", err) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + if !ok { + retErr := fmt.Errorf("oauth token was not created by vela") + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // return a 200 indicating token is valid and created by the server's OAuth app + c.JSON(http.StatusOK, "oauth token was created by vela") +} diff --git a/mock/server/authentication.go b/mock/server/authentication.go index ea7bb76a1..36cc159f5 100644 --- a/mock/server/authentication.go +++ b/mock/server/authentication.go @@ -87,3 +87,17 @@ func validateToken(c *gin.Context) { c.JSON(http.StatusOK, "vela-server") } + +// validateOAuthToken returns mock response for a http GET. +// +// Don't pass "Authorization" in header to receive an unauthorized error message. +func validateOAuthToken(c *gin.Context) { + err := "error" + + token := c.Request.Header.Get("Authorization") + if len(token) == 0 { + c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &err}) + } + + c.JSON(http.StatusOK, "oauth token was created by vela") +} diff --git a/mock/server/server.go b/mock/server/server.go index 62f8d9302..0df494596 100644 --- a/mock/server/server.go +++ b/mock/server/server.go @@ -136,6 +136,7 @@ func FakeHandler() http.Handler { e.GET("/authenticate", getAuthenticate) e.POST("/authenticate/token", getAuthenticateFromToken) e.GET("/validate-token", validateToken) + e.GET("/validate-oauth", validateOAuthToken) // mock endpoint for queue credentials e.GET("/api/v1/queue/info", getQueueCreds) diff --git a/router/router.go b/router/router.go index 01304d6e2..29ed0de1b 100644 --- a/router/router.go +++ b/router/router.go @@ -81,6 +81,9 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // Validate Server Token endpoint r.GET("/validate-token", claims.Establish(), auth.ValidateServerToken) + // Validate OAuth Token endpoint + r.GET("/validate-oauth", claims.Establish(), auth.ValidateOAuthToken) + // Version endpoint r.GET("/version", api.Version) diff --git a/scm/github/authentication.go b/scm/github/authentication.go index fe9080497..93ff5ff8e 100644 --- a/scm/github/authentication.go +++ b/scm/github/authentication.go @@ -106,6 +106,32 @@ func (c *client) AuthenticateToken(ctx context.Context, r *http.Request) (*libra return nil, errors.New("no token provided") } + // validate that the token was not created by vela + ok, err := c.ValidateOAuthToken(ctx, token) + if err != nil { + return nil, fmt.Errorf("unable to validate oauth token: %w", err) + } + + if ok { + return nil, errors.New("token must not be created by vela") + } + + u, err := c.Authorize(ctx, token) + if err != nil { + return nil, err + } + + return &library.User{ + Name: &u, + Token: &token, + }, nil +} + +// ValidateOAuthToken takes a user oauth integration token and +// validates that it was created by the Vela OAuth app. +// In essence, the function expects either a 200 or 404 from the GitHub API and returns +// error in any other failure case. +func (c *client) ValidateOAuthToken(ctx context.Context, token string) (bool, error) { // create http client to connect to GitHub API transport := github.BasicAuthTransport{ Username: c.config.ClientID, @@ -123,7 +149,7 @@ func (c *client) AuthenticateToken(ctx context.Context, r *http.Request) (*libra // parse the provided url into url type enterpriseURL, err := url.Parse(c.config.Address) if err != nil { - return nil, err + return false, err } // set the base and upload url client.BaseURL = enterpriseURL @@ -140,24 +166,11 @@ func (c *client) AuthenticateToken(ctx context.Context, r *http.Request) (*libra case http.StatusNotFound: break default: - return nil, err + return false, err } } else if err != nil { - return nil, err - } - - // return error if the token was created by Vela - if resp.StatusCode != http.StatusNotFound { - return nil, errors.New("token must not be created by vela") + return false, err } - u, err := c.Authorize(ctx, token) - if err != nil { - return nil, err - } - - return &library.User{ - Name: &u, - Token: &token, - }, nil + return resp.StatusCode == http.StatusOK, nil } diff --git a/scm/github/authentication_test.go b/scm/github/authentication_test.go index 3965430eb..4655bc362 100644 --- a/scm/github/authentication_test.go +++ b/scm/github/authentication_test.go @@ -338,15 +338,15 @@ func TestGithub_AuthenticateToken(t *testing.T) { got, err := client.AuthenticateToken(_context.TODO(), context.Request) if resp.Code != http.StatusOK { - t.Errorf("Authenticate returned %v, want %v", resp.Code, http.StatusOK) + t.Errorf("AuthenticateToken returned %v, want %v", resp.Code, http.StatusOK) } if err != nil { - t.Errorf("Authenticate returned err: %v", err) + t.Errorf("AuthenticateToken returned err: %v", err) } if !reflect.DeepEqual(got, want) { - t.Errorf("Authenticate is %v, want %v", got, want) + t.Errorf("AuthenticateToken is %v, want %v", got, want) } } @@ -374,15 +374,15 @@ func TestGithub_AuthenticateToken_Invalid(t *testing.T) { got, err := client.AuthenticateToken(_context.TODO(), context.Request) if resp.Code != http.StatusOK { - t.Errorf("Authenticate returned %v, want %v", resp.Code, http.StatusOK) + t.Errorf("AuthenticateToken returned %v, want %v", resp.Code, http.StatusOK) } if err == nil { - t.Errorf("Authenticate did not return err") + t.Errorf("AuthenticateToken did not return err") } if got != nil { - t.Errorf("Authenticate is %v, want nil", got) + t.Errorf("AuthenticateToken is %v, want nil", got) } } @@ -423,6 +423,109 @@ func TestGithub_AuthenticateToken_Vela_OAuth(t *testing.T) { } } +func TestGithub_ValidateOAuthToken_Valid(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/validate-oauth", nil) + + token := "foobar" + want := true + scmResponseCode := http.StatusOK + + engine.POST("/api/v3/applications/foo/token", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(scmResponseCode) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + client, _ := NewTest(s.URL) + + // run test + got, err := client.ValidateOAuthToken(_context.TODO(), token) + + if got != want { + t.Errorf("ValidateOAuthToken returned %v, want %v", got, want) + } + + if err != nil { + t.Errorf("ValidateOAuthToken returned err: %v", err) + } +} + +func TestGithub_ValidateOAuthToken_Invalid(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/validate-oauth", nil) + + token := "foobar" + want := false + // 404 from the mocked github server indicates an invalid oauth token + scmResponseCode := http.StatusNotFound + + engine.POST("/api/v3/applications/foo/token", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(scmResponseCode) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + client, _ := NewTest(s.URL) + + // run test + got, err := client.ValidateOAuthToken(_context.TODO(), token) + + if got != want { + t.Errorf("ValidateOAuthToken returned %v, want %v", got, want) + } + + if err != nil { + t.Errorf("ValidateOAuthToken returned err: %v", err) + } +} + +func TestGithub_ValidateOAuthToken_Error(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/validate-oauth", nil) + + token := "foobar" + want := false + scmResponseCode := http.StatusInternalServerError + + engine.POST("/api/v3/applications/foo/token", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(scmResponseCode) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + client, _ := NewTest(s.URL) + + // run test + got, err := client.ValidateOAuthToken(_context.TODO(), token) + + if got != want { + t.Errorf("ValidateOAuthToken returned %v, want %v", got, want) + } + + if err == nil { + t.Errorf("ValidateOAuthToken did not return err") + } +} + func TestGithub_LoginWCreds(t *testing.T) { // setup context gin.SetMode(gin.TestMode) @@ -446,7 +549,7 @@ func TestGithub_LoginWCreds(t *testing.T) { _, err := client.Login(_context.TODO(), context.Writer, context.Request) if resp.Code != http.StatusOK { - t.Errorf("Enable returned %v, want %v", resp.Code, http.StatusOK) + t.Errorf("Login returned %v, want %v", resp.Code, http.StatusOK) } if err != nil { diff --git a/scm/service.go b/scm/service.go index 7d015dca2..4586c1e26 100644 --- a/scm/service.go +++ b/scm/service.go @@ -34,6 +34,10 @@ type Service interface { // the OAuth workflow for the session using PAT Token AuthenticateToken(context.Context, *http.Request) (*library.User, error) + // ValidateOAuthToken defines a function that validates + // an OAuth access token was created by Vela + ValidateOAuthToken(context.Context, string) (bool, error) + // Login defines a function that begins // the OAuth workflow for the session. Login(context.Context, http.ResponseWriter, *http.Request) (string, error)