2024-03-25 23:25:50 +00:00
|
|
|
package actions
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2024-09-12 22:57:45 +00:00
|
|
|
"strings"
|
2024-03-25 23:25:50 +00:00
|
|
|
"time"
|
|
|
|
|
2024-09-25 07:06:21 +00:00
|
|
|
"code.gitea.io/gitea/modules/json"
|
2024-09-30 16:11:45 +00:00
|
|
|
"code.gitea.io/gitea/modules/jwtx"
|
2024-03-25 23:25:50 +00:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2024-03-26 02:41:24 +00:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2024-03-25 23:25:50 +00:00
|
|
|
"code.gitea.io/gitea/modules/web"
|
2024-09-29 23:58:23 +00:00
|
|
|
|
2024-03-25 23:25:50 +00:00
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
)
|
|
|
|
|
|
|
|
type oidcRoutes struct {
|
2024-09-30 16:11:45 +00:00
|
|
|
signingKey jwtx.JWTSigningKey
|
2024-03-26 02:41:24 +00:00
|
|
|
openIDConfiguration openIDConfiguration
|
2024-03-25 23:25:50 +00:00
|
|
|
}
|
|
|
|
|
2024-03-26 02:41:24 +00:00
|
|
|
type openIDConfiguration struct {
|
|
|
|
Issuer string `json:"issuer"`
|
|
|
|
JwksURI string `json:"jwks_uri"`
|
|
|
|
SubjecTypesSupported []string `json:"subject_types_supported"`
|
|
|
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
|
|
|
ClaimsSupported []string `json:"claims_supported"`
|
|
|
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
|
|
|
ScopesSupported []string `json:"scopes_supported"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func OIDCRoutes(prefix string) *web.Route {
|
2024-03-25 23:25:50 +00:00
|
|
|
m := web.NewRoute()
|
|
|
|
|
2024-09-12 22:57:45 +00:00
|
|
|
prefix = strings.TrimPrefix(prefix, "/")
|
|
|
|
|
2024-09-30 16:11:45 +00:00
|
|
|
rawKey, err := jwtx.LoadOrCreateAsymmetricKey(setting.Actions.JWTSigningPrivateKeyFile, setting.Actions.JWTSigningAlgorithm)
|
2024-03-25 23:25:50 +00:00
|
|
|
if err != nil {
|
2024-09-30 16:11:45 +00:00
|
|
|
log.Fatal("error loading jwt: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := jwtx.CreateJWTSigningKey(setting.Actions.JWTSigningAlgorithm, rawKey)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("error parsing jwt: %v", err)
|
2024-03-25 23:25:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
r := oidcRoutes{
|
2024-09-30 16:11:45 +00:00
|
|
|
signingKey: key,
|
2024-03-26 02:41:24 +00:00
|
|
|
openIDConfiguration: openIDConfiguration{
|
|
|
|
Issuer: setting.AppURL + setting.AppSubURL + prefix, // TODO: how do i check the public domain?
|
|
|
|
JwksURI: setting.AppURL + setting.AppSubURL + prefix + "/.well-known/jwks", // TODO: how do i check the public domain?
|
|
|
|
SubjecTypesSupported: []string{"public", "pairwise"},
|
|
|
|
ResponseTypesSupported: []string{"id_token"},
|
|
|
|
ClaimsSupported: []string{
|
|
|
|
"sub", "aud", "exp", "iat", "iss", "jti", "nbf", "ref", "sha", "repository", "repository_id",
|
|
|
|
"repository_owner", "repository_owner_id", "enterprise", "enterprise_id", "run_id",
|
|
|
|
"run_number", "run_attempt", "actor", "actor_id", "workflow", "workflow_ref", "workflow_sha",
|
|
|
|
"head_ref", "base_ref", "event_name", "ref_type", "ref_protected", "environment",
|
|
|
|
"environment_node_id", "job_workflow_ref", "job_workflow_sha", "repository_visibility",
|
2024-09-29 23:34:26 +00:00
|
|
|
"runner_environment", "issuer_scope",
|
|
|
|
},
|
2024-03-26 02:41:24 +00:00
|
|
|
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
|
|
|
ScopesSupported: []string{"openid"},
|
|
|
|
},
|
2024-03-25 23:25:50 +00:00
|
|
|
}
|
2024-03-26 02:41:24 +00:00
|
|
|
m.Get("", ArtifactContexter(), r.getToken)
|
|
|
|
m.Get("/.well-known/jwks", r.getJWKS)
|
|
|
|
m.Get("/.well-known/openid-configuration", r.getOpenIDConfiguration)
|
2024-03-25 23:25:50 +00:00
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
// sample JWT github gave me:
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// "jti": "18b3eacd-6330-47e2-a113-604effd4cf91",
|
|
|
|
// "sub": "repo:thefinn93/actions-test:ref:refs/heads/main",
|
|
|
|
// "aud": "CUSTOM_AUDIENCE",
|
|
|
|
// "ref": "refs/heads/main",
|
|
|
|
// "sha": "5d92aa8ec38679dbd7b90eee4ea8dfa342e2c6e3",
|
|
|
|
// "repository": "thefinn93/actions-test",
|
|
|
|
// "repository_owner": "thefinn93",
|
|
|
|
// "repository_owner_id": "692970",
|
|
|
|
// "run_id": "8414792498",
|
|
|
|
// "run_number": "2",
|
|
|
|
// "run_attempt": "1",
|
|
|
|
// "repository_visibility": "public",
|
|
|
|
// "repository_id": "777026467",
|
|
|
|
// "actor_id": "692970",
|
|
|
|
// "actor": "thefinn93",
|
|
|
|
// "workflow": ".github/workflows/test.yaml",
|
|
|
|
// "head_ref": "",
|
|
|
|
// "base_ref": "",
|
|
|
|
// "event_name": "push",
|
|
|
|
// "ref_protected": "false",
|
|
|
|
// "ref_type": "branch",
|
|
|
|
// "workflow_ref": "thefinn93/actions-test/.github/workflows/test.yaml@refs/heads/main",
|
|
|
|
// "workflow_sha": "5d92aa8ec38679dbd7b90eee4ea8dfa342e2c6e3",
|
|
|
|
// "job_workflow_ref": "thefinn93/actions-test/.github/workflows/test.yaml@refs/heads/main",
|
|
|
|
// "job_workflow_sha": "5d92aa8ec38679dbd7b90eee4ea8dfa342e2c6e3",
|
|
|
|
// "runner_environment": "github-hosted",
|
|
|
|
// "iss": "https://token.actions.githubusercontent.com",
|
|
|
|
// "nbf": 1711337835,
|
|
|
|
// "exp": 1711338735,
|
|
|
|
// "iat": 1711338435
|
|
|
|
// }
|
|
|
|
func (o oidcRoutes) getToken(ctx *ArtifactContext) {
|
2024-03-26 02:41:24 +00:00
|
|
|
task := ctx.ActionTask
|
|
|
|
|
2024-09-30 18:11:41 +00:00
|
|
|
aud := ctx.Req.URL.Query().Get("audience")
|
2024-09-30 16:11:45 +00:00
|
|
|
|
2024-03-26 02:41:24 +00:00
|
|
|
if err := task.Job.LoadRun(ctx); err != nil {
|
|
|
|
log.Error("Error loading run: %v", err)
|
|
|
|
ctx.Error(http.StatusInternalServerError, "Error loading run")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := task.Job.Run.LoadAttributes(ctx); err != nil {
|
|
|
|
log.Error("Error loading attributes: %v", err)
|
|
|
|
ctx.Error(http.StatusInternalServerError, "Error loading attributes")
|
2024-03-25 23:25:50 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// there's probably better ways get some of these values
|
|
|
|
repo := fmt.Sprintf("%s/%s", task.Job.Run.Repo.OwnerName, task.Job.Run.Repo.Name)
|
|
|
|
repositoryVisibility := "public"
|
|
|
|
if task.Job.Run.Repo.IsPrivate { // are there options other than public and private?
|
|
|
|
repositoryVisibility = "private"
|
|
|
|
}
|
|
|
|
iat := time.Now()
|
2024-03-26 02:41:24 +00:00
|
|
|
|
2024-09-30 16:11:45 +00:00
|
|
|
token := jwt.NewWithClaims(jwt.GetSigningMethod(setting.Actions.JWTSigningAlgorithm), jwt.MapClaims{
|
2024-03-25 23:25:50 +00:00
|
|
|
"jti": uuid.New().String(),
|
|
|
|
"sub": fmt.Sprintf("repo:%s:ref:%s", repo, task.Job.Run.Ref),
|
2024-09-30 16:11:45 +00:00
|
|
|
"aud": aud,
|
2024-03-25 23:25:50 +00:00
|
|
|
"ref": task.Job.Run.Ref,
|
|
|
|
"sha": task.Job.Run.CommitSHA,
|
|
|
|
"repository": repo,
|
|
|
|
"repository_owner": task.Job.Run.Repo.OwnerName,
|
|
|
|
"repository_owner_id": task.Job.Run.Repo.OwnerID,
|
2024-03-26 02:41:24 +00:00
|
|
|
"run_id": task.Job.RunID,
|
2024-09-30 16:11:45 +00:00
|
|
|
"run_number": task.Job.Run.Index,
|
2024-03-25 23:25:50 +00:00
|
|
|
"run_attempt": 0, // TODO: how do i check this?
|
|
|
|
"repository_visibility": repositoryVisibility,
|
|
|
|
"repository_id": task.Job.Run.Repo.ID,
|
|
|
|
"actor_id": task.Job.Run.TriggerUserID,
|
|
|
|
"actor": task.Job.Run.TriggerUser.Name,
|
|
|
|
"workflow": fmt.Sprintf(".forgejo/workflow/%s", task.Job.Run.WorkflowID), // TODO: remove hard-coded prefix, fetch it from wherever that data is stored
|
|
|
|
"head_ref": "", // this is empty in my GH test. Maybe we should take it out here?
|
|
|
|
"base_ref": "", // this is empty in my GH test. Maybe we should take it out here?
|
|
|
|
"event_name": task.Job.Run.TriggerEvent,
|
|
|
|
"ref_protected": false, // TODO: how do i check this?
|
|
|
|
"ref_type": "branch", // TODO: how do i check this?
|
|
|
|
"workflow_ref": fmt.Sprintf("%s/.forgejo/workflow/%s@%s", repo, task.Job.Run.WorkflowID, task.Job.Run.Ref),
|
2024-09-30 16:11:45 +00:00
|
|
|
"workflow_sha": task.Job.Run.CommitSHA,
|
2024-03-25 23:25:50 +00:00
|
|
|
"job_workflow_ref": fmt.Sprintf("%s/.forgejo/workflow/%s@%s", repo, task.Job.Run.WorkflowID, task.Job.Run.Ref),
|
2024-09-30 16:11:45 +00:00
|
|
|
"job_workflow_sha": task.Job.Run.CommitSHA,
|
|
|
|
"runner_environment": "self-hosted", // not sure what this should be set to, github will have either "github-hosted" or "self-hosted"
|
2024-10-01 00:54:51 +00:00
|
|
|
"iss": setting.AppURL + setting.AppSubURL + "/api/actions_idtoken",
|
2024-03-26 03:07:25 +00:00
|
|
|
"nbf": jwt.NewNumericDate(iat),
|
2024-10-01 05:07:08 +00:00
|
|
|
"exp": jwt.NewNumericDate(iat.Add(setting.Actions.JWTExpirationTime)),
|
2024-03-26 03:07:25 +00:00
|
|
|
"iat": jwt.NewNumericDate(iat),
|
2024-10-01 05:07:08 +00:00
|
|
|
})
|
2024-03-25 23:25:50 +00:00
|
|
|
|
2024-09-30 16:11:45 +00:00
|
|
|
signedJWT, err := token.SignedString(o.signingKey.SignKey())
|
2024-03-25 23:25:50 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("Error signing JWT: %v", err)
|
|
|
|
ctx.Error(http.StatusInternalServerError, "Error signing JWT")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
|
|
"count": 0, // TODO: unclear what this is, github gave me a value of 1857
|
2024-09-13 23:53:09 +00:00
|
|
|
"value": signedJWT,
|
2024-03-25 23:25:50 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-03-26 02:41:24 +00:00
|
|
|
func (o oidcRoutes) getJWKS(resp http.ResponseWriter, req *http.Request) {
|
2024-09-12 22:57:45 +00:00
|
|
|
resp.Header().Set("Content-Type", "application/json")
|
2024-09-30 16:11:45 +00:00
|
|
|
|
|
|
|
jwk, err := o.signingKey.ToJWK()
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Error converting signing key to JWK: %v", err)
|
|
|
|
http.Error(resp, "error converting signing key to JWT", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jwk["use"] = "sig"
|
|
|
|
|
|
|
|
jwks := map[string][]map[string]string{
|
|
|
|
"keys": {
|
|
|
|
jwk,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.NewEncoder(resp).Encode(jwks)
|
2024-03-26 02:41:24 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("error encoding jwks response: ", err)
|
|
|
|
http.Error(resp, "error encoding jwks response", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2024-03-25 23:25:50 +00:00
|
|
|
|
2024-03-26 02:41:24 +00:00
|
|
|
func (o oidcRoutes) getOpenIDConfiguration(resp http.ResponseWriter, req *http.Request) {
|
2024-09-12 22:57:45 +00:00
|
|
|
resp.Header().Set("Content-Type", "application/json")
|
2024-03-26 02:41:24 +00:00
|
|
|
err := json.NewEncoder(resp).Encode(o.openIDConfiguration)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("error encoding jwks response: ", err)
|
|
|
|
http.Error(resp, "error encoding jwks response", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2024-03-25 23:25:50 +00:00
|
|
|
}
|