diff --git a/api/authenticate.go b/api/authenticate.go index 5e1416f21..4f9bf6431 100644 --- a/api/authenticate.go +++ b/api/authenticate.go @@ -7,19 +7,18 @@ package api import ( "encoding/base64" "fmt" - "net/http" - "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/token" "github.com/go-vela/server/source" "github.com/go-vela/server/util" "github.com/go-vela/types" + "github.com/google/uuid" "github.com/sirupsen/logrus" + "net/http" "github.com/go-vela/types/library" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) // swagger:operation GET /authenticate authenticate GetAuthenticate @@ -211,3 +210,33 @@ func AuthenticateType(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, r) } + +// AuthenticateToken represents the API handler to +// process a user logging in using PAT to Vela from +// the API +func AuthenticateToken(c *gin.Context) { + newUser, err := source.FromContext(c).AuthenticateToken(c.Writer, c.Request) + if err != nil { + retErr := fmt.Errorf("unable to authenticate user: %w", err) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + newUser.SetActive(true) + + // We don't need refresh token for this scenario + // We only need access token and are configured based on the config defined + m := c.MustGet("metadata").(*types.Metadata) + at, err := token.CreateAccessToken(newUser, m.Vela.AccessTokenDuration) + + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", newUser.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + } + + // return the user with their jwt access token + c.JSON(http.StatusOK, library.Login{Token: &at}) +} diff --git a/router/router.go b/router/router.go index 8956c7bb3..ec7494800 100644 --- a/router/router.go +++ b/router/router.go @@ -81,6 +81,7 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { authenticate.GET("", api.Authenticate) authenticate.GET("/:type", api.AuthenticateType) authenticate.GET("/:type/:port", api.AuthenticateType) + authenticate.POST("/token", api.AuthenticateToken) } // API endpoints diff --git a/source/github/authentication.go b/source/github/authentication.go index 22614ab4b..071becbf9 100644 --- a/source/github/authentication.go +++ b/source/github/authentication.go @@ -93,3 +93,21 @@ func (c *client) Authenticate(w http.ResponseWriter, r *http.Request, oAuthState Token: &token.AccessToken, }, nil } + +// AuthenticateToken completes the authentication workflow +// for the session and returns the remote user details. +func (c *client) AuthenticateToken(w http.ResponseWriter, r *http.Request) (*library.User, error) { + logrus.Trace("Authenticating user via token") + + token := r.Header.Get("Token") + + u, err := c.Authorize(token) + if err != nil { + return nil, err + } + + return &library.User{ + Name: &u, + Token: &token, + }, nil +} diff --git a/source/github/authentication_test.go b/source/github/authentication_test.go index fce8b7e9d..805a6422d 100644 --- a/source/github/authentication_test.go +++ b/source/github/authentication_test.go @@ -307,6 +307,83 @@ func TestGithub_Login(t *testing.T) { } } +func TestGithub_Authenticate_Token(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodPost, "/authenticate/token", nil) + context.Request.Header.Set("Token", "foo") + + engine.GET("/api/v3/user", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/user.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + want := new(library.User) + want.SetName("octocat") + want.SetToken("foo") + + client, _ := NewTest(s.URL) + + // run test + got, err := client.AuthenticateToken(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Authenticate returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("Authenticate returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Authenticate is %v, want %v", got, want) + } +} + +func TestGithub_Authenticate_Invalid_Token(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodPost, "/authenticate/token", nil) + context.Request.Header.Set("Token", "foo") + + // setup mock server + engine.GET("/api/v3/user", func(c *gin.Context) { + c.Status(http.StatusNotFound) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup client + client, _ := NewTest(s.URL) + + // run test + got, err := client.AuthenticateToken(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Authenticate returned %v, want %v", resp.Code, http.StatusOK) + } + + if err == nil { + t.Errorf("Authenticate did not return err") + } + + if got != nil { + t.Errorf("Authenticate is %v, want nil", got) + } +} + func TestGithub_LoginWCreds(t *testing.T) { // setup context gin.SetMode(gin.TestMode) diff --git a/source/source.go b/source/source.go index 9e8aa3aff..a9495ad33 100644 --- a/source/source.go +++ b/source/source.go @@ -22,6 +22,11 @@ type Service interface { // Authenticate defines a function that completes // the OAuth workflow for the session. Authenticate(http.ResponseWriter, *http.Request, string) (*library.User, error) + + // AuthenticateToken defines a function that completes + // the OAuth workflow for the session using PAT Token + AuthenticateToken(http.ResponseWriter, *http.Request) (*library.User, error) + // Login defines a function that begins // the OAuth workflow for the session. Login(http.ResponseWriter, *http.Request) (string, error)