Files
gitocean/services/auth/authorized_integration_oidc_test.go
T
Mathieu FenniakandMathieu Fenniak cf087a2f12 feat: ability to edit authorized integration in web UI (#12601)
Extends the UI introduced in #12558 to have edit capabilities.  (not in scope: "Add" for a new Authorized Integration will be the next update to this UI; `create-authorized-integration` CLI is still the only way to create a new record)

This PR includes a few refactoring steps.  The goal of these steps is to have `services/auth` be a single entrypoint for validating, inserting, or updating an authorized integration.  Some logic is moved out of `services/authz` because it is not authorization related, and some is moved out of `services/auth/method` to allow it to be reused during validation without creating a cyclical module dependency.

This PR also adds comprehensive validation to the more complex fields in the authorized integration, such as the issuer and claim rules.  This validation applies to the `forgejo admin user create-authorized-integration` CLI as well.

The visible UI is the same as #12558, but with a "Save" button, and the ability to display errors:

![Screenshot 2026-05-16 at 15-43-20 Authorized Integrations - Forgejo Beyond coding. We Forge](/attachments/ffaf60e2-3652-429b-a815-b339100f05f8)

## 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...
  - [x] `make pr-go` before pushing

### Tests for JavaScript changes

- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [x] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.
    - Documentation is on my TODO list and will be completed before release.

### Release notes

- [x] 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.
- [ ] 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.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12601
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
2026-05-17 18:33:39 +02:00

274 lines
12 KiB
Go

// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package auth
import (
"testing"
"forgejo.org/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Real-world Forgejo Actions .well-known/openid-configuration from Forgejo v15
const forgejoOIDC = `
{
"issuer": "https://example.org/api/actions",
"jwks_uri": "https://example.org/api/actions/.well-known/keys",
"subject_types_supported": [
"public"
],
"response_types_supported": [
"id_token"
],
"claims_supported": [
"sub",
"aud",
"exp",
"iat",
"iss",
"nbf",
"actor",
"base_ref",
"event_name",
"head_ref",
"ref",
"ref_protected",
"ref_type",
"repository",
"repository_owner",
"run_attempt",
"run_id",
"run_number",
"sha",
"workflow",
"workflow_ref"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}
`
// Real-world Forgejo Actions JWKS from Forgejo v15
const forgejoJWKS = `
{
"keys": [
{
"alg": "RS256",
"e": "AQAB",
"kid": "SNNttXGzw6l53JC158lXddjSjQ5bJ9bdTTqTi12gaLY",
"kty": "RSA",
"n": "7RL963BVzemasfImhlR3KUX97YdA7g3SBnq_ZLzcdxLXPGDhsnSoxMX7gY30b1qpQlML8yiAyz_gxUydiVlqpEEPypR9lfKtZXv4JTM-X2rccegcreUyfFJnFuzVUoY7SVEzAulLUwqP8MH8kxDI7JZRQ8_JIjm9IxEuWCSc3XnVxNCTS2XEdHsug_Kt6SQdcH8xL9U2w0EHAUna9KkLAl6_PzBg1JxIQDHQtfp_CN7YNyoyilH88XAGEeQm0fLz6GH7hhyw6y1b9NprYIxNrdD4Pb1b66j4K--bCJy530UmEAlfLbCiDCh4k78TPUnU_YwwT4ujC0t28zoHNB3Y0w",
"use": "sig"
}
]
}
`
// Real-world GitHub Actions /.well-known/openid-configuration
const githubOIDC = `
{
"issuer": "https://token.actions.githubusercontent.com",
"jwks_uri": "https://token.actions.githubusercontent.com/.well-known/jwks",
"subject_types_supported": [
"public",
"pairwise"
],
"response_types_supported": [
"id_token"
],
"claims_supported": [
"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",
"runner_environment",
"issuer_scope",
"check_run_id"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}
`
// Real-world GitHub Actions JWKS
const githubJWKS = `
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "cc413527-173f-5a05-976e-9c52b1d7b431",
"n": "w4M936N3ZxNaEblcUoBm-xu0-V9JxNx5S7TmF0M3SBK-2bmDyAeDdeIOTcIVZHG-ZX9N9W0u1yWafgWewHrsz66BkxXq3bscvQUTAw7W3s6TEeYY7o9shPkFfOiU3x_KYgOo06SpiFdymwJflRs9cnbaU88i5fZJmUepUHVllP2tpPWTi-7UA3AdP3cdcCs5bnFfTRKzH2W0xqKsY_jIG95aQJRBDpbiesefjuyxcQnOv88j9tCKWzHpJzRKYjAUM6OPgN4HYnaSWrPJj1v41eEkFM1kORuj-GSH2qMVD02VklcqaerhQHIqM-RjeHsN7G05YtwYzomE5G-fZuwgvQ",
"e": "AQAB"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "38826b17-6a30-5f9b-b169-8beb8202f723",
"n": "5Manmy-zwsk3wEftXNdKFZec4rSWENW4jTGevlvAcU9z3bgLBogQVvqYLtu9baVm2B3rfe5onadobq8po5UakJ0YsTiiEfXWdST7YI2Sdkvv-hOYMcZKYZ4dFvuSO1vQ2DgEkw_OZNiYI1S518MWEcNxnPU5u67zkawAGsLlmXNbOylgVfBRJrG8gj6scr-sBs4LaCa3kg5IuaCHe1pB-nSYHovGV_z0egE83C098FfwO1dNZBWeo4Obhb5Z-ZYFLJcZfngMY0zJnCVNmpHQWOgxfGikh3cwi4MYrFrbB4NTlxbrQ3bL-rGKR5X318veyDlo8Dyz2KWMobT4wB9U1Q",
"e": "AQAB",
"x5c": [
"MIIDKzCCAhOgAwIBAgIUDnwm6eRIqGFA3o/P1oBrChvx/nowDQYJKoZIhvcNAQELBQAwJTEjMCEGA1UEAwwaYWN0aW9ucy5zZWxmLXNpZ25lZC5naXRodWIwHhcNMjQwMTIzMTUyNTM2WhcNMzQwMTIwMTUyNTM2WjAlMSMwIQYDVQQDDBphY3Rpb25zLnNlbGYtc2lnbmVkLmdpdGh1YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOTGp5svs8LJN8BH7VzXShWXnOK0lhDVuI0xnr5bwHFPc924CwaIEFb6mC7bvW2lZtgd633uaJ2naG6vKaOVGpCdGLE4ohH11nUk+2CNknZL7/oTmDHGSmGeHRb7kjtb0Ng4BJMPzmTYmCNUudfDFhHDcZz1Obuu85GsABrC5ZlzWzspYFXwUSaxvII+rHK/rAbOC2gmt5IOSLmgh3taQfp0mB6Lxlf89HoBPNwtPfBX8DtXTWQVnqODm4W+WfmWBSyXGX54DGNMyZwlTZqR0FjoMXxopId3MIuDGKxa2weDU5cW60N2y/qxikeV99fL3sg5aPA8s9iljKG0+MAfVNUCAwEAAaNTMFEwHQYDVR0OBBYEFIPALo5VanJ6E1B9eLQgGO+uGV65MB8GA1UdIwQYMBaAFIPALo5VanJ6E1B9eLQgGO+uGV65MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGS0hZE+DqKIRi49Z2KDOMOaSZnAYgqq6ws9HJHT09MXWlMHB8E/apvy2ZuFrcSu14ZLweJid+PrrooXEXEO6azEakzCjeUb9G1QwlzP4CkTcMGCw1Snh3jWZIuKaw21f7mp2rQ+YNltgHVDKY2s8AD273E8musEsWxJl80/MNvMie8Hfh4n4/Xl2r6t1YPmUJMoXAXdTBb0hkPy1fUu3r2T+1oi7Rw6kuVDfAZjaHupNHzJeDOg2KxUoK/GF2/M2qpVrd19Pv/JXNkQXRE4DFbErMmA7tXpp1tkXJRPhFui/Pv5H9cPgObEf9x6W4KnCXzT3ReeeRDKF8SqGTPELsc="
],
"x5t": "ykNaY4qM_ta4k2TgZOCEYLkcYlA"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "38E9B30B3A023A1B72309921A69A42FCC496C42C",
"n": "tEq2Fp9HcdT5MwMsB_UTm8j_woJJLi3sA-y0RX2tioTm581seyfvOH6lJ5JmHVtS-_fb8B2tRT1pznHQSNq14PsJdu9bp5egbWmIz-5RvhqoM-oKem_MJENCNFuqXijRLT47FRdfH3inqde1vJlA_JJHCqYMKIpHH7kqNFYcCpwr0vk80Hc2rTyL0uBXI7NqBZbtUgNoyucWO5O7QQrPNOmlr-GI8aFckFRfobCaCOiH9qW02FtkV74fwBGVCNhNf3a1CK81-O8xEGimvVydI_pQA5B8QqVuQjY_ntOu555HdirA0hKkY6fsE9eZCMFmWDHZ2kSWLjhabxWxIzSzXQ",
"e": "AQAB",
"x5c": [
"MIIDrDCCApSgAwIBAgIQbuIOJTcGQ4GOQs29F1/uLzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEyt2c3RzLXZzdHNnaHJ0LWdoLXZzby1vYXV0aC52aXN1YWxzdHVkaW8uY29tMB4XDTI1MDkxMTE5MzcxOFoXDTI3MDkxMTE5NDcxOFowNjE0MDIGA1UEAxMrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALRKthafR3HU+TMDLAf1E5vI/8KCSS4t7APstEV9rYqE5ufNbHsn7zh+pSeSZh1bUvv32/AdrUU9ac5x0EjateD7CXbvW6eXoG1piM/uUb4aqDPqCnpvzCRDQjRbql4o0S0+OxUXXx94p6nXtbyZQPySRwqmDCiKRx+5KjRWHAqcK9L5PNB3Nq08i9LgVyOzagWW7VIDaMrnFjuTu0EKzzTppa/hiPGhXJBUX6Gwmgjoh/altNhbZFe+H8ARlQjYTX92tQivNfjvMRBopr1cnSP6UAOQfEKlbkI2P57TrueeR3YqwNISpGOn7BPXmQjBZlgx2dpEli44Wm8VsSM0s10CAwEAAaOBtTCBsjAOBgNVHQ8BAf8EBAMCBaAwCQYDVR0TBAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwNgYDVR0RBC8wLYIrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTAfBgNVHSMEGDAWgBQLxdObdnfWzaBcxau87tdSUtEuQjAdBgNVHQ4EFgQUC8XTm3Z31s2gXMWrvO7XUlLRLkIwDQYJKoZIhvcNAQELBQADggEBAD2Eo703wXQgB2vJn/RwTTcHGkeMkYXm0mWCxOSh4iCKVvqypBJrmLzRkMMJN0/10qIGciYWUl6EkL7yj48tpXXH01Ep0ONDdo9UYmKGp81Z4j3u3FBJTVQSdj2tnPOPZlYWaBkerIkcIeyWBRKvne1UBaobbk84epfBmUfAMFmyJEk+x+q7cqmsbjDtdrmhiWaInqCijpS2dW2MitJ5F7tBBS26SMTqLQteA2IOwIW1BMlYIPuSO3dKn/rYVS8RjL+x+MxP98vla5sichoEZwVWnXiXgFZ4n/asGqc+Da9q6ILLtInvgI5bi7kjJJ2ARTRC5/a+J/v3EL+t8SdnOO8="
],
"x5t": "OOmzCzoCOhtyMJkhpppC_MSWxCw"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "4F3E9AD8C9A6F5EB3173006F4FA630E28F43DCE9",
"n": "tGevqhkBGn8NB0dKxs8Ddxhn-xZPm55svcSlkJZEOwDOXDLl_0-iVOVKNJfcHHLHvMqa6zh2DDcpAWZi2FpeBAJupsrymqwzllxOODWKWoVIoaIjOO7h1JLiF9Knwuq-o6BPtKdwOT-bOrXRzChMtQsc5C1Auex-D0Z6loObBuK1Lkm0RK9ISQsLqBEwq8g0OOupI_shU1r2rT2G0nkZ0CvxVlQeUGShFi8Mdys2s5LPqBwjC4LKwjk8moWQV32KEccbTPKxnG_539DxRglHJgHPHisSVGsfZIUXi2chtXdQHZPdVve8ZRmknCykZtkJ6K87llSUXi7oyzhCIZdiUQ",
"e": "AQAB",
"x5c": [
"MIIDrDCCApSgAwIBAgIQPQS35v3ITW6fNLO8GX5QBjANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEyt2c3RzLXZzdHNnaHJ0LWdoLXZzby1vYXV0aC52aXN1YWxzdHVkaW8uY29tMB4XDTI1MDgwNjE0MTEzMloXDTI3MDgwNjE0MjEzMlowNjE0MDIGA1UEAxMrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALRnr6oZARp/DQdHSsbPA3cYZ/sWT5uebL3EpZCWRDsAzlwy5f9PolTlSjSX3Bxyx7zKmus4dgw3KQFmYthaXgQCbqbK8pqsM5ZcTjg1ilqFSKGiIzju4dSS4hfSp8LqvqOgT7SncDk/mzq10cwoTLULHOQtQLnsfg9GepaDmwbitS5JtESvSEkLC6gRMKvINDjrqSP7IVNa9q09htJ5GdAr8VZUHlBkoRYvDHcrNrOSz6gcIwuCysI5PJqFkFd9ihHHG0zysZxv+d/Q8UYJRyYBzx4rElRrH2SFF4tnIbV3UB2T3Vb3vGUZpJwspGbZCeivO5ZUlF4u6Ms4QiGXYlECAwEAAaOBtTCBsjAOBgNVHQ8BAf8EBAMCBaAwCQYDVR0TBAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwNgYDVR0RBC8wLYIrdnN0cy12c3RzZ2hydC1naC12c28tb2F1dGgudmlzdWFsc3R1ZGlvLmNvbTAfBgNVHSMEGDAWgBRKoYOga736JYE15vT7b4gWjC1hwTAdBgNVHQ4EFgQUSqGDoGu9+iWBNeb0+2+IFowtYcEwDQYJKoZIhvcNAQELBQADggEBAJVZIPtoZUlvqgu+Pl0nj8WopA8iuy1m7JRg5fg+bOIGFhXFR8+mH8prpeodjUQ40q2Hq6IwnVir+G56zVwAPf2HHksqdp8be9qjkTjD0mJorPCt/lumrKoNGOVmYffYuIyr73hwsl8fN6sGjAyXLFBkozE4s5ssbeodFxiYE1A61SXnzldC00M7qWleMWjTUBixiZ+R/eroddkLNBGDv9ewDrTQv1ipNec89+Wi7Wb6SAXNxBADiC5kVlFylBgHo3oZNg3KFzZS01REyc4zdH7v1wfZzilLluI6ygTyYRYpJCsKrX5D9JW196f2PCzcs+VXMfneRDnvyfjep7Y1Pi8="
],
"x5t": "Tz6a2Mmm9esxcwBvT6Yw4o9D3Ok"
}
]
}
`
// Real-world .well-known/openid-configuration extracted from a AWS Federated Web Identity endpoint.
const awsOIDC = `
{
"claims_supported": [
"sub",
"iss",
"aud",
"exp",
"iat",
"jti",
"https://sts.amazonaws.com/"
],
"id_token_signing_alg_values_supported": [
"RS256",
"ES384"
],
"issuer": "https://a103a2cc-b461-473d-84fe-6c4f6d45af88.tokens.sts.global.api.aws",
"jwks_uri": "https://a103a2cc-b461-473d-84fe-6c4f6d45af88.tokens.sts.global.api.aws/.well-known/jwks.json",
"subject_types_supported": [
"public"
]
}
`
// Real-world JWKS extracted from a AWS Federated Web Identity endpoint.
const awsJWKS = `
{
"keys": [
{
"e": "AQAB",
"kid": "RSA_0",
"kty": "RSA",
"n": "3AvB0UECoYssZEgSMTa4SYvfqstJxkhbBBSKAFRUW6f_McJ9CAXkTi6YkG0NGm77ZIRW12_gOLKZJUHWp9CMAbmk0O4sMIx8K6Ap7-6qjkt7FYvl4mkQVJd-pU-yE3SJn0S5xEbCYXulgrrGN8POysTblqN0BfrdDAYTVhWQ47rbm--3QrRcVN9XCjlMBVXYauaN6KlszKL6NTe7GWilauYBsVHw7d4ekliuEGGA6zJNGz595KD7yofRc1euFs86KgiFj0mpudCqG39jIlBJ4vZSJPw1Rsvhg8THqlxhmurVYr9TuckLJa5fpEL78xGs3Ar4GIM6w0sxLDbdY-KdCQ",
"use": "sig"
},
{
"alg": "ES384",
"crv": "P-384",
"kid": "EC384_0",
"kty": "EC",
"use": "sig",
"x": "ad_olFw0n3XBA114sefjlirPf2gX6bKqT-kD2lQzfQzkWW1TetKIUWah3md-UgV9",
"y": "w6GzW2Oen4G7Ei1bFaDkBpPSulvkSznb6YtG79NWK9UjgDqfN6am9lUs-bF8VN7v"
}
]
}
`
func TestParseOpenIDConfiguration(t *testing.T) {
t.Run("Forgejo", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDConfiguration
data := []byte(forgejoOIDC)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Equal(t, "https://example.org/api/actions/.well-known/keys", retval.JwksURI)
})
t.Run("GitHub", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDConfiguration
data := []byte(githubOIDC)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Equal(t, "https://token.actions.githubusercontent.com/.well-known/jwks", retval.JwksURI)
})
t.Run("AWS", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDConfiguration
data := []byte(awsOIDC)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Equal(t, "https://a103a2cc-b461-473d-84fe-6c4f6d45af88.tokens.sts.global.api.aws/.well-known/jwks.json", retval.JwksURI)
})
}
func TestParseJSONWebKeySet(t *testing.T) {
t.Run("Forgejo", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDKeys
data := []byte(forgejoJWKS)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Len(t, retval.Keys, 1)
})
t.Run("GitHub", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDKeys
data := []byte(githubJWKS)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Len(t, retval.Keys, 4)
})
t.Run("AWS", func(t *testing.T) {
var retval AuthorizedIntegrationOpenIDKeys
data := []byte(awsJWKS)
require.NoError(t, json.Unmarshal(data, &retval))
assert.Len(t, retval.Keys, 2)
})
}