diff --git a/routers/api/actions/id_token.go b/routers/api/actions/id_token.go index 5eacc77fc9..ab9b2c8d22 100644 --- a/routers/api/actions/id_token.go +++ b/routers/api/actions/id_token.go @@ -6,6 +6,7 @@ package actions import ( "fmt" "net/http" + "slices" "strings" "time" @@ -72,6 +73,7 @@ func IDTokenContexter() func(next http.Handler) http.Handler { if err != nil { log.Error("Error runner api parsing custom claims: %v", err) ctx.Error(http.StatusInternalServerError, "Error runner api parsing custom claims") + return } task, err := actions.GetTaskByID(req.Context(), authorizationTokenClaims.TaskID) @@ -99,6 +101,13 @@ func IDTokenContexter() func(next http.Handler) http.Handler { return } + generateIDTokenScp := fmt.Sprintf("generate_id_token:%d:%d", task.Job.RunID, task.Job.ID) + scp := strings.Split(authorizationTokenClaims.Scp, " ") + if !slices.Contains(scp, generateIDTokenScp) { + ctx.Error(http.StatusForbidden, "missing scp generate_id_token") + return + } + audience := req.URL.Query().Get("audience") if audience == "" { // Default to organization that owns the repo if no audience is provided diff --git a/tests/integration/api_actions_id_token_test.go b/tests/integration/api_actions_id_token_test.go index d5c7301ad1..53a0a5b269 100644 --- a/tests/integration/api_actions_id_token_test.go +++ b/tests/integration/api_actions_id_token_test.go @@ -47,6 +47,8 @@ func TestActionsIDToken(t *testing.T) { token, err := actions_service.CreateAuthorizationToken(task, gitCtx, true) require.NoError(t, err) + tokenWithoutOIDCAccess, err := actions_service.CreateAuthorizationToken(task, gitCtx, false) + require.NoError(t, err) // get JWKs information req := NewRequest(t, "GET", "/api/actions/.well-known/keys") @@ -118,6 +120,13 @@ func TestActionsIDToken(t *testing.T) { doAssertions("testingAud", claims) }) + t.Run("with token that doesn't support OIDC", func(t *testing.T) { + req = NewRequest(t, "GET", "/api/actions/_apis/pipelines/workflows/792/idtoken?placeholder=true").AddTokenAuth(tokenWithoutOIDCAccess) + resp = MakeRequest(t, req, http.StatusInternalServerError) + assert.Contains(t, resp.Body.String(), "Error runner api parsing custom claims") + assert.NotContains(t, resp.Body.String(), "value") // must not leak an actual `getTokenResponse` + }) + t.Run("with no auth header", func(t *testing.T) { req = NewRequest(t, "GET", "/api/actions/_apis/pipelines/workflows/792/idtoken?placeholder=true&audience=testingAud") resp = MakeRequest(t, req, http.StatusUnauthorized)