Authorized Integrations is a new feature to allow users to define external systems which can generate JSON Web Tokens (JWTs) that Forgejo will trust in order to perform API access on behalf of that user. This is an authentication mechanism that requires zero preconfiguration of shared secrets, and instead establishes trust through short-lived secrets (JWTs) that are signed by the issuer, signatures are validated by comparison with published public keys, and a public-keys retrieved through well-known HTTP endpoints secured with TLS verification. The primary goal of Authorized Integrations is to support a mechanism for Forgejo Actions to receive elevated, but controlled, additional access to Forgejo. More details as to what the end result will look like are available in the [design proposal](https://codeberg.org/forgejo/forgejo/issues/3571#issuecomment-13268004) on #3571. This PR adds the core database storage and authentication verification for Authorized Integrations, with these capabilities: - An Authorized Integration is resolved by a unique key of an "issuer" and an "audience". The value of "issuer" is defined by the remote integration, and the value of "audience" will incorporate a unique identifier generated by Forgejo. - Example issuer: `https://token.actions.githubusercontent.com/` is the issuer for GitHub JWTs - Example audience: `https://forgejo.example.org/-/mfenniak/authorized-integration/6cc55ba0` is the expected format for a random audience field that Forgejo will generate. - JWTs can contain any number of claims, which are represented as a JSON object; Forgejo can validate these with a flexible policy. - eg. a claim may be `{"sub": "repo:coolguy/forgejo-runner-testrepo:pull_request"}` indicating that an OIDC token was received from an Actions execution in a specific repo on a specific event. - Authorized Integrations support a `ClaimRules` system which allows claim equal, glob, and nested object inspection. - `{"claim":"sub","comparison":"eq","value":"repo:mfenniak/forgejo-runner-testrepo:pull_request"}` -- would validate that `sub` exactly equals the specific value - `{"claim":"sub","comparison":"glob","value":"repo:mfenniak/forgejo-runner-testrepo:*"}` -- would validate that `sub` matches the given string prefix but allow any event - When a JWT is received on an incoming API call, Forgejo retrieves the Authorized Integration from the DB (if present), validates the token signature against a remote JWKS, validates the claims, and grants API access as the user with a permission scope defined on the Authorized Integration. In addition to the unit testing provided here, this PR has been manually integration tested against three JWT issuing systems: Forgejo Actions, GitHub Actions, and AWS STS GetWebIdentityToken. Careful consideration has been made of these security concerns: - SSRF attacks against Forgejo are prevented by: - having a blocklist on remote HTTP validation requests which prevent access to internal network resources, - ensuring that authorized integrations are created by users with matching issuers, before attempting to validate tokens - Resource utilization attacks against Forgejo are reduced by limiting the possible size of external metadata requests; when fetching `/.well-known/openid-configuration` and `jkws_uri`'s from remote, untrusted servers, a maximum response size of 16 kB is enforced - Only well-known secure assymmetric JWT signing algorithms are supported -- in particular, the sketchy `none` JWT algorithm isn't supported. - JWT validation is covered by extensive unit tests, covering validation of all JWT timestamps, validation of the issuers, validation of the issuer's documented supported signing algorithms. This PR serves as a core, and many enhancements are required for this to be a usable system for users. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [ ] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - Documentation updates for new config entries will be authored. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. - Marking not visible as there's no mechanism to interact with this backend yet. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12261 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> |
||
|---|---|---|
| .. | ||
| foreign_key_utils.go | ||
| index_utils.go | ||
| index_utils_test.go | ||
| main_test.go | ||
| migrate.go | ||
| migrate_test.go | ||
| README.md | ||
| v14a_actions-approval-and-trust.go | ||
| v14a_actions-approval-and-trust_test.go | ||
| v14a_add-action_task-index.go | ||
| v14a_add-foreign-keys-collaboration.go | ||
| v14a_add-foreign-keys-collaboration_test.go | ||
| v14a_add-foreign-keys-forgejo_auth_token.go | ||
| v14a_add-foreign-keys-forgejo_auth_token_test.go | ||
| v14a_add-foreign-keys-pull_request-1.go | ||
| v14a_add-foreign-keys-pull_request-1_test.go | ||
| v14a_add-forgejo-migrations-table.go | ||
| v14a_ap-change-fedi-handle-structure.go | ||
| v14a_migrate_task_secrets.go | ||
| v14a_migrate_task_secrets_test.go | ||
| v14a_migrate_webhook_authorization.go | ||
| v14a_migrate_webhook_authorization_test.go | ||
| v14a_remove-is-deleted-column-from-activity-action-table.go | ||
| v14a_remove-is-deleted-column-from-activity-action-table_test.go | ||
| v14a_rework-notification.go | ||
| v14a_rm-repository-issue-stat-fields.go | ||
| v14a_set_remote_user_prohibit_login.go | ||
| v14b_action-reindexing.go | ||
| v14b_action-run-add-workflow-directory.go | ||
| v14b_add-action_run-preexecutionerrorcode.go | ||
| v14b_rm-repository-actionrun-stat-fields.go | ||
| v15a_remove-softdelete-action_runner_token.go | ||
| v15a_remove-softdelete-action_runner_token_test.go | ||
| v15b_add-access_token-owned-repos.go | ||
| v15b_add-access_token_resource.go | ||
| v15b_add-ephemeral_runner.go | ||
| v15b_add-foreign-keys-action_runner_token.go | ||
| v15b_add-foreign-keys-action_runner_token_test.go | ||
| v15b_add-runner_request_key.go | ||
| v15c_add_job_handle.go | ||
| v15c_add_mirror_remoteaddressauth.go | ||
| v15c_add_schedule_spec_time_zones.go | ||
| v15c_fix-project-sorting-unique-constraints.go | ||
| v16a_add_authorized_integration.go | ||
forgejo_migrations
Forgejo has three database migration mechanisms:
models/gitea_migrations- Original database schema migrations from Forgejo's legacy as a fork of Gitea.
- A linear set of migrations referenced in
models/gitea_migrations/migrations.go, each represented by a number (eg. migration 304). - The current version is recorded in the database in the table
version.
models/forgejo_migrations_legacy- The next 50-ish database schema migrations reflecting change in Forgejo's database structure between Forgejo v7.0 and v14.0
- A linear set of migrations referenced in
models/forgejo_migrations_legacy/migrate.go, each represented by a number (eg. migration 43). - The current version is recorded in the database in the table
forgejo_version.
models/forgejo_migrations- The most recent database schema migrations, reflecting change in the v14.0 release cycle and onwards into the future.
- Each migration is identified by the filename it is stored in.
- The applied migrations are recorded in the database in the table
forgejo_migration.
forgejo_migrations is designed to reduce code conflicts when multiple developers may be making schema migrations in close succession, which it does by avoiding having one code file with a long array of migrations. Instead, each file in models/forgejo_migrations registers itself as a migration, and its filename indicates the order that migration will be applied.
Files in forgejo_migrations must:
- Define an
initfunction which registers a function to be invoked for the migration. - Follow the naming convention:
- The letter
v - A number, representing the development cycle that the migration was created in
- A letter, indicating any required migration ordering
- The character
_(underscore) - A short descriptive identifier for the migration
- The letter
For example, valid migration file names would look like this:
v14a_add-threaded-comments.gov14a_add-federated-emojis.gov14b_fix-threaded-comments-index.go
Migration Ordering
Forgejo executes registered migrations in forgejo_migrations in the strings.Compare() ordering of their filename.
There are edge cases where migrations may not be executed in this exact order:
- If a schema change is backported to an earlier Forgejo release. For example, if a bugfix during the v15 development cycle was backported into a v14 patch release, then a migration labeled
v15a_fix-unusual-data-corruption.gocould be applied during a v14 software upgrade. In the future when a v15 software release occurs, that migration will be identified as already applied and will be skipped. - If a developer working on Forgejo switches between different branches with different schema migrations.
- If the contents of the
forgejo_migrationsdatabase table are changed outside of Forgejo modifying it.
Creating a new Migration
First, determine the filename for your migration. In general, you create a new migration by starting a file with the same prefix as the most recent migration present. If v14a_add-forgejo-migrations-table.go was the last file, most of the time you can create your migration with the same v14a_... prefix.
There are two exceptions:
- After the release branch is cut for a release, increment the version in the migration. If v14 was cut, you would start
v15a_...as the next migration. - If your migration requires that an earlier migration is complete first, you would increment the letter in the prefix. If you were modifying the table created by
v14a_add-forgejo-migrations-table.go, then you would name your migrationv14b_....
Once you've determined the migration filename, then you can copy this template into the file:
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations
import (
"forgejo.org/modules/timeutil"
"xorm.io/xorm"
)
func init() {
registerMigration(&Migration{
Description: "short sentence describing this migration",
Upgrade: myMigrationFunction, // rename
})
}
func myMigrationFunction(x *xorm.Engine) error {
// add migration logic here
//
// to prevent `make watch` from recording this migration as done when it
// isn't authored yet, returh an error until the implementation is done
return errors.New("not implemented yet")
}
And now it's up to you to write the contents of your migration function.
Development Notes
Once migrations are executed, a record of their execution is stored in the database table forgejo_migration.
=> SELECT * FROM forgejo_migration;
id | created_unix
-----------------------------------+--------------
v14a_add-forgejo-migrations-table | 1760402451
v14a_example-other-migration | 1760402453
v14b_another-example | 1760402455
v15a_add-something-cool | 1760402456
v15a_another-example-again | 1760402457
If your migration successfully executes once, it will be recorded in this table and it will never execute again, even if you change the migration code. It is common during development to need to re-run a migration, in which case you can delete the record that you're working on developing. The migration will be re-run as soon as the Forgejo server is restarted:
=> DELETE FROM forgejo_migration WHERE id = 'v15a_another-example-again';