Skip to content

Commit 48872d1

Browse files
bohdeearl-warren
authored andcommitted
allow the actions user to login via the jwt token (go-gitea#32527)
We have some actions that leverage the Gitea API that began receiving 401 errors, with a message that the user was not found. These actions use the `ACTIONS_RUNTIME_TOKEN` env var in the actions job to authenticate with the Gitea API. The format of this env var in actions jobs changed with go-gitea/pull/28885 to be a JWT (with a corresponding update to `act_runner`) Since it was a JWT, the OAuth parsing logic attempted to parse it as an OAuth token, and would return user not found, instead of falling back to look up the running task and assigning it to the actions user. Make ACTIONS_RUNTIME_TOKEN in action runners could be used, attempting to parse Oauth JWTs. The code to parse potential old `ACTION_RUNTIME_TOKEN` was kept in case someone is running an older version of act_runner that doesn't support the Actions JWT. (cherry picked from commit 407b6e6) Conflicts: services/auth/oauth2.go trivial context conflicts because OAuth2 scopes are in Forgejo and not yet in Gitea
1 parent 1c04f8f commit 48872d1

File tree

4 files changed

+105
-4
lines changed

4 files changed

+105
-4
lines changed

models/fixtures/action_task.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
-
2+
id: 46
3+
attempt: 3
4+
runner_id: 1
5+
status: 3 # 3 is the status code for "cancelled"
6+
started: 1683636528
7+
stopped: 1683636626
8+
repo_id: 4
9+
owner_id: 1
10+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
11+
is_fork_pull_request: 0
12+
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
13+
token_salt: eeeeeeee
14+
token_last_eight: eeeeeeee
15+
log_filename: artifact-test2/2f/47.log
16+
log_in_storage: 1
17+
log_length: 707
18+
log_size: 90179
19+
log_expired: 0
120
-
221
id: 47
322
job_id: 192

services/actions/auth.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
8383
return 0, fmt.Errorf("split token failed")
8484
}
8585

86-
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
86+
return TokenToTaskID(parts[1])
87+
}
88+
89+
// TokenToTaskID returns the TaskID associated with the provided JWT token
90+
func TokenToTaskID(token string) (int64, error) {
91+
parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) {
8792
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
8893
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
8994
}
@@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
9398
return 0, err
9499
}
95100

96-
c, ok := token.Claims.(*actionsClaims)
97-
if !token.Valid || !ok {
101+
c, ok := parsedToken.Claims.(*actionsClaims)
102+
if !parsedToken.Valid || !ok {
98103
return 0, fmt.Errorf("invalid token claim")
99104
}
100105

services/auth/oauth2.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"code.gitea.io/gitea/modules/setting"
1919
"code.gitea.io/gitea/modules/timeutil"
2020
"code.gitea.io/gitea/modules/web/middleware"
21+
"code.gitea.io/gitea/services/actions"
2122
"code.gitea.io/gitea/services/auth/source/oauth2"
2223
)
2324

@@ -94,6 +95,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) (int64, stri
9495
return grant.UserID, grantScopes
9596
}
9697

98+
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
99+
func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
100+
// Verify the task exists
101+
task, err := actions_model.GetTaskByID(ctx, taskID)
102+
if err != nil {
103+
return false
104+
}
105+
106+
// Verify that it's running
107+
return task.Status == actions_model.StatusRunning
108+
}
109+
97110
// OAuth2 implements the Auth interface and authenticates requests
98111
// (API requests only) by looking for an OAuth token in query parameters or the
99112
// "Authorization" header.
@@ -137,8 +150,17 @@ func parseToken(req *http.Request) (string, bool) {
137150
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
138151
// Let's see if token is valid.
139152
if strings.Contains(tokenSHA, ".") {
140-
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
153+
// First attempt to decode an actions JWT, returning the actions user
154+
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
155+
if CheckTaskIsRunning(ctx, taskID) {
156+
store.GetData()["IsActionsToken"] = true
157+
store.GetData()["ActionsTaskID"] = taskID
158+
return user_model.ActionsUserID
159+
}
160+
}
141161

162+
// Otherwise, check if this is an OAuth access token
163+
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
142164
if uid != 0 {
143165
store.GetData()["IsApiToken"] = true
144166
if grantScopes != "" {

services/auth/oauth2_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/unittest"
11+
user_model "code.gitea.io/gitea/models/user"
12+
"code.gitea.io/gitea/modules/web/middleware"
13+
"code.gitea.io/gitea/services/actions"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestUserIDFromToken(t *testing.T) {
20+
require.NoError(t, unittest.PrepareTestDatabase())
21+
22+
t.Run("Actions JWT", func(t *testing.T) {
23+
const RunningTaskID = 47
24+
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
25+
require.NoError(t, err)
26+
27+
ds := make(middleware.ContextData)
28+
29+
o := OAuth2{}
30+
uid := o.userIDFromToken(context.Background(), token, ds)
31+
assert.Equal(t, int64(user_model.ActionsUserID), uid)
32+
assert.Equal(t, true, ds["IsActionsToken"])
33+
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
34+
})
35+
}
36+
37+
func TestCheckTaskIsRunning(t *testing.T) {
38+
require.NoError(t, unittest.PrepareTestDatabase())
39+
cases := map[string]struct {
40+
TaskID int64
41+
Expected bool
42+
}{
43+
"Running": {TaskID: 47, Expected: true},
44+
"Missing": {TaskID: 1, Expected: false},
45+
"Cancelled": {TaskID: 46, Expected: false},
46+
}
47+
48+
for name := range cases {
49+
c := cases[name]
50+
t.Run(name, func(t *testing.T) {
51+
actual := CheckTaskIsRunning(context.Background(), c.TaskID)
52+
assert.Equal(t, c.Expected, actual)
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)