Files
Max P. a4908d4af4 enhance(actions): add OIDC workload identity support (#39052)
Adds OpenID Connect workload identity to Gitea Actions: jobs declaring
`id-token: write` can request short-lived tokens to authenticate to
cloud providers and other services without stored secrets. Tokens follow
GitHub's format, so clients like `@actions/core` and existing trust
policies carry over.

- Issuer at `/api/actions/oidc` with discovery and JWKS, signed with a
dedicated RS256 key
- GitHub's claims, including `workflow_ref`, `job_workflow_ref` and the
immutable `repo:owner@id/repo@id:ref:...` subject
- `id-token: write` or `write-all` grant it, default permissions and
fork pull requests never get it, owner and repository maximum
permissions can disable it
- Reusable workflows record the exact ref they were loaded from, and
dispatch rejects refs that only resolve through a same-named tag
- Generated signing keys are written atomically, and generated ES384 and
ES512 OAuth2 keys use their own curves instead of P-256
- `github.ref_type` is `branch` for pull request runs, like on GitHub

Fixes https://github.com/go-gitea/gitea/issues/26383
Fixes https://github.com/go-gitea/gitea/issues/33681
Docs: https://gitea.com/gitea/docs/pulls/526
Assisted-by: ChatGPT:gpt-5.6-sol, Claude Code:claude-opus-5-5
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-18 07:08:55 +00:00

67 lines
2.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"errors"
"net/http"
"gitea.dev/modules/auth/httpauth"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
)
func oidcConfiguration(resp http.ResponseWriter, req *http.Request) {
issuer := actions_service.OIDCIssuer()
context.NewBaseContext(resp, req).JSON(http.StatusOK, map[string]any{
"issuer": issuer,
"jwks_uri": issuer + "/jwks",
"response_types_supported": []string{"id_token"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{actions_service.OIDCSigningAlgorithm},
"claims_supported": actions_service.OIDCClaimsSupported(),
"scopes_supported": []string{"openid"},
})
}
func oidcKeys(resp http.ResponseWriter, req *http.Request) {
ctx := context.NewBaseContext(resp, req)
key, err := actions_service.OIDCSigningKey()
if err != nil {
log.Error("Actions OIDC signing key: %v", err)
ctx.HTTPError(http.StatusInternalServerError)
return
}
jwk, err := key.ToJWK()
if err != nil {
log.Error("Actions OIDC JWK: %v", err)
ctx.HTTPError(http.StatusInternalServerError)
return
}
jwk["use"] = "sig"
ctx.JSON(http.StatusOK, map[string]any{"keys": []map[string]string{jwk}})
}
func oidcToken(resp http.ResponseWriter, req *http.Request) {
ctx := context.NewBaseContext(resp, req)
token := ""
if auth, ok := httpauth.ParseAuthorizationHeader(req.Header.Get("Authorization")); ok && auth.BearerToken != nil {
token = auth.BearerToken.Token
}
idToken, err := actions_service.CreateOIDCToken(ctx, token, ctx.FormString("audience"))
if errors.Is(err, util.ErrPermissionDenied) {
ctx.Resp.Header().Set("WWW-Authenticate", "Bearer")
ctx.HTTPError(http.StatusUnauthorized)
return
} else if err != nil {
log.Error("Actions OIDC token: %v", err)
ctx.HTTPError(http.StatusInternalServerError)
return
}
ctx.Resp.Header().Set("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, map[string]string{"value": idToken})
}