Skip to content

Commit

Permalink
feat: endpoint for validating oauth token source (#972)
Browse files Browse the repository at this point in the history
  • Loading branch information
plyr4 authored Sep 27, 2023
1 parent f3acd78 commit aa6f468
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 24 deletions.
75 changes: 75 additions & 0 deletions api/auth/validate_oauth.go
Original file line number Diff line number Diff line change
@@ -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")
}
14 changes: 14 additions & 0 deletions mock/server/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
1 change: 1 addition & 0 deletions mock/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
47 changes: 30 additions & 17 deletions scm/github/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
}
117 changes: 110 additions & 7 deletions scm/github/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions scm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit aa6f468

Please sign in to comment.