diff options
author | Patrick Bajao <ebajao@gitlab.com> | 2022-04-27 03:04:21 +0000 |
---|---|---|
committer | Patrick Bajao <ebajao@gitlab.com> | 2022-04-27 03:04:21 +0000 |
commit | 73a8361eb3c49979d4ff5d9f1ba82a9935b22915 (patch) | |
tree | 0cc59978a7ec94dacca7d32bccd179638b35745d | |
parent | 67164bdf589ec2729f20ef4434cf83d1535a9d24 (diff) | |
parent | 71231b3393c2e81558b74c8a574e297b2a1f900f (diff) | |
download | gitlab-shell-73a8361eb3c49979d4ff5d9f1ba82a9935b22915.tar.gz |
Merge branch 'id-gitlabnet-jwt' into 'main'
Add JWT token to GitLab Rails request
See merge request gitlab-org/gitlab-shell!596
-rw-r--r-- | client/client_test.go | 58 | ||||
-rw-r--r-- | client/gitlabnet.go | 25 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 |
4 files changed, 77 insertions, 9 deletions
diff --git a/client/client_test.go b/client/client_test.go index 186f919..4b26898 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -10,13 +10,19 @@ import ( "path" "strings" "testing" + "time" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-shell/client/testserver" "gitlab.com/gitlab-org/gitlab-shell/internal/testhelper" ) +var ( + secret = []byte("sssh, it's a secret") +) + func TestClients(t *testing.T) { testhelper.PrepareTestRootDir(t) @@ -57,12 +63,10 @@ func TestClients(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { url := tc.server(t, buildRequests(t, tc.relativeURLRoot)) - secret := "sssh, it's a secret" - httpClient, err := NewHTTPClientWithOpts(url, tc.relativeURLRoot, tc.caFile, "", 1, nil) require.NoError(t, err) - client, err := NewGitlabNetClient("", "", secret, httpClient) + client, err := NewGitlabNetClient("", "", string(secret), httpClient) require.NoError(t, err) testBrokenRequest(t, client) @@ -71,6 +75,7 @@ func TestClients(t *testing.T) { testMissing(t, client) testErrorMessage(t, client) testAuthenticationHeader(t, client) + testJWTAuthenticationHeader(t, client) }) } } @@ -160,7 +165,7 @@ func testAuthenticationHeader(t *testing.T, client *GitlabNetClient) { header, err := base64.StdEncoding.DecodeString(string(responseBody)) require.NoError(t, err) - require.Equal(t, "sssh, it's a secret", string(header)) + require.Equal(t, secret, header) }) t.Run("Authentication headers for POST", func(t *testing.T) { @@ -175,7 +180,44 @@ func testAuthenticationHeader(t *testing.T, client *GitlabNetClient) { header, err := base64.StdEncoding.DecodeString(string(responseBody)) require.NoError(t, err) - require.Equal(t, "sssh, it's a secret", string(header)) + require.Equal(t, secret, header) + }) +} + +func testJWTAuthenticationHeader(t *testing.T, client *GitlabNetClient) { + verifyJWTToken := func(t *testing.T, response *http.Response) { + responseBody, err := io.ReadAll(response.Body) + require.NoError(t, err) + + claims := &jwt.RegisteredClaims{} + token, err := jwt.ParseWithClaims(string(responseBody), claims, func(token *jwt.Token) (interface{}, error) { + return secret, nil + }) + require.NoError(t, err) + require.True(t, token.Valid) + require.Equal(t, "gitlab-shell", claims.Issuer) + require.Equal(t, time.Now().Truncate(time.Second), claims.IssuedAt.Time, time.Second) + require.Equal(t, time.Now().Truncate(time.Second).Add(time.Minute), claims.ExpiresAt.Time, time.Second) + } + + t.Run("JWT authentication headers for GET", func(t *testing.T) { + response, err := client.Get(context.Background(), "/jwt_auth") + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + verifyJWTToken(t, response) + }) + + t.Run("JWT authentication headers for POST", func(t *testing.T) { + response, err := client.Post(context.Background(), "/jwt_auth", map[string]string{}) + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + verifyJWTToken(t, response) }) } @@ -209,6 +251,12 @@ func buildRequests(t *testing.T, relativeURLRoot string) []testserver.TestReques }, }, { + Path: "/api/v4/internal/jwt_auth", + Handler: func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, r.Header.Get(apiSecretHeaderName)) + }, + }, + { Path: "/api/v4/internal/error", Handler: func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/client/gitlabnet.go b/client/gitlabnet.go index f71c110..ac056aa 100644 --- a/client/gitlabnet.go +++ b/client/gitlabnet.go @@ -11,13 +11,18 @@ import ( "strings" "time" + "github.com/golang-jwt/jwt/v4" + "gitlab.com/gitlab-org/labkit/log" ) const ( - internalApiPath = "/api/v4/internal" - secretHeaderName = "Gitlab-Shared-Secret" - defaultUserAgent = "GitLab-Shell" + internalApiPath = "/api/v4/internal" + secretHeaderName = "Gitlab-Shared-Secret" + apiSecretHeaderName = "Gitlab-Shell-Api-Request" + defaultUserAgent = "GitLab-Shell" + jwtTTL = time.Minute + jwtIssuer = "gitlab-shell" ) type ErrorResponse struct { @@ -121,10 +126,22 @@ func (c *GitlabNetClient) DoRequest(ctx context.Context, method, path string, da if user != "" && password != "" { request.SetBasicAuth(user, password) } + secretBytes := []byte(c.secret) - encodedSecret := base64.StdEncoding.EncodeToString([]byte(c.secret)) + encodedSecret := base64.StdEncoding.EncodeToString(secretBytes) request.Header.Set(secretHeaderName, encodedSecret) + claims := jwt.RegisteredClaims{ + Issuer: jwtIssuer, + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtTTL)), + } + tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretBytes) + if err != nil { + return nil, err + } + request.Header.Set(apiSecretHeaderName, tokenString) + request.Header.Add("Content-Type", "application/json") request.Header.Add("User-Agent", c.userAgent) request.Close = true @@ -3,6 +3,7 @@ module gitlab.com/gitlab-org/gitlab-shell go 1.17 require ( + github.com/golang-jwt/jwt/v4 v4.4.1 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mattn/go-shellwords v1.0.11 @@ -316,6 +316,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= |