git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: add SourceHut (sr.ht) provider (#2359)

* Add SourceHut (sr.ht) provider * fix changelog entry Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Jan Larwig <jan@larwig.com>

commita88306be980a4866edae676f8c976ab94f23eec6 authorConrad Hoffmann <1226676+bitfehler@users.noreply.github.com> dateTue 22 Jul 2025, 08:16 (1 year ago) parent4d17bc1
6 files changed+218−1
CHANGELOG.md+1 docs/docs/configuration/providers/sourcehut.md+25 pkg/apis/options/providers.go+3 providers/providers.go+4−1 providers/srht.go+108 providers/srht_test.go+77
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111- [#2615](https://github.com/oauth2-proxy/oauth2-proxy/pull/2615) feat(cookies): add option to set a limit on the number of per-request CSRF cookies oauth2-proxy sets (@bh-tt)
1212- [#2605](https://github.com/oauth2-proxy/oauth2-proxy/pull/2605) fix: show login page on broken cookie (@Primexz)
1313- [#2743](https://github.com/oauth2-proxy/oauth2-proxy/pull/2743) feat: allow use more possible google admin-sdk api scopes (@BobDu)
14+- [#2359](https://github.com/oauth2-proxy/oauth2-proxy/pull/2359) feat: add SourceHut (sr.ht) provider(@bitfehler)
1415
1516# V7.10.0
1617
docs/docs/configuration/providers/sourcehut.mdnew file+25
@@ -0,0 +1,25 @@
1+---
2+id: sourcehut
3+title: SourceHut
4+---
5+
6+1. Create a new OAuth client: https://meta.sr.ht/oauth2
7+2. Under `Redirection URI` enter the correct URL, i.e.
8+ `https://internal.yourcompany.com/oauth2/callback`
9+
10+To use the provider, start with `--provider=sourcehut`.
11+
12+If you are hosting your own SourceHut instance, make sure you set the following
13+to the appropriate URLs:
14+
15+```shell
16+ --login-url="https://<meta.your.instance>/oauth2/authorize"
17+ --redeem-url="https://<meta.your.instance>/oauth2/access-token"
18+ --profile-url="https://<meta.your.instance>/query"
19+ --validate-url="https://<meta.your.instance>/profile"
20+```
21+
22+The default configuration allows everyone with an account to authenticate.
23+Restricting access is currently only supported by
24+[email](#email-authentication).
25+
pkg/apis/options/providers.go+3
@@ -147,6 +147,9 @@ const (
147147
148148 // OIDCProvider is the provider type for OIDC
149149 OIDCProvider ProviderType = "oidc"
150+
151+ // SourceHutProvider is the provider type for SourceHut
152+ SourceHutProvider ProviderType = "sourcehut"
150153)
151154
152155type KeycloakOptions struct {
providers/providers.go+4−1
@@ -67,6 +67,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
6767 return NewNextcloudProvider(providerData), nil
6868 case options.OIDCProvider:
6969 return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
70+ case options.SourceHutProvider:
71+ return NewSourceHutProvider(providerData), nil
7072 default:
7173 return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
7274 }
@@ -183,7 +185,8 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
183185func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
184186 switch providerType {
185187 case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
186− options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
188+ options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
189+ options.NextCloudProvider, options.SourceHutProvider:
187190 return false, nil
188191 case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider, options.MicrosoftEntraIDProvider:
189192 return true, nil
providers/srht.gonew file+108
@@ -0,0 +1,108 @@
1+package providers
2+
3+import (
4+ "bytes"
5+ "context"
6+ "fmt"
7+ "net/url"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
12+)
13+
14+type SourceHutProvider struct {
15+ *ProviderData
16+}
17+
18+var _ Provider = (*SourceHutProvider)(nil)
19+
20+const (
21+ SourceHutProviderName = "SourceHut"
22+ SourceHutDefaultScope = "meta.sr.ht/PROFILE:RO"
23+)
24+
25+var (
26+ // Default Login URL for SourceHut.
27+ // Pre-parsed URL of https://meta.sr.ht/oauth2/authorize.
28+ SourceHutDefaultLoginURL = &url.URL{
29+ Scheme: "https",
30+ Host: "meta.sr.ht",
31+ Path: "/oauth2/authorize",
32+ }
33+
34+ // Default Redeem URL for SourceHut.
35+ // Pre-parsed URL of https://meta.sr.ht/oauth2/access-token.
36+ SourceHutDefaultRedeemURL = &url.URL{
37+ Scheme: "https",
38+ Host: "meta.sr.ht",
39+ Path: "/oauth2/access-token",
40+ }
41+
42+ // Default Profile URL for SourceHut.
43+ // Pre-parsed URL of https://meta.sr.ht/query.
44+ SourceHutDefaultProfileURL = &url.URL{
45+ Scheme: "https",
46+ Host: "meta.sr.ht",
47+ Path: "/query",
48+ }
49+
50+ // Default Validation URL for SourceHut.
51+ // Pre-parsed URL of https://meta.sr.ht/profile.
52+ SourceHutDefaultValidateURL = &url.URL{
53+ Scheme: "https",
54+ Host: "meta.sr.ht",
55+ Path: "/profile",
56+ }
57+)
58+
59+// NewSourceHutProvider creates a SourceHutProvider using the passed ProviderData
60+func NewSourceHutProvider(p *ProviderData) *SourceHutProvider {
61+ p.setProviderDefaults(providerDefaults{
62+ name: SourceHutProviderName,
63+ loginURL: SourceHutDefaultLoginURL,
64+ redeemURL: SourceHutDefaultRedeemURL,
65+ profileURL: SourceHutDefaultProfileURL,
66+ validateURL: SourceHutDefaultValidateURL,
67+ scope: SourceHutDefaultScope,
68+ })
69+
70+ return &SourceHutProvider{ProviderData: p}
71+}
72+
73+// EnrichSession uses the SourceHut userinfo endpoint to populate the session's
74+// email and username.
75+func (p *SourceHutProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
76+ json, err := requests.New(p.ProfileURL.String()).
77+ WithContext(ctx).
78+ WithMethod("POST").
79+ SetHeader("Content-Type", "application/json").
80+ SetHeader("Authorization", "Bearer "+s.AccessToken).
81+ WithBody(bytes.NewBufferString(`{"query": "{ me { username, email } }"}`)).
82+ Do().
83+ UnmarshalSimpleJSON()
84+ if err != nil {
85+ logger.Errorf("failed making request %v", err)
86+ return err
87+ }
88+
89+ email, err := json.GetPath("data", "me", "email").String()
90+ if err != nil {
91+ return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
92+ }
93+ s.Email = email
94+
95+ username, err := json.GetPath("data", "me", "username").String()
96+ if err != nil {
97+ return fmt.Errorf("unable to extract username from userinfo endpoint: %v", err)
98+ }
99+ s.PreferredUsername = username
100+ s.User = username
101+
102+ return nil
103+}
104+
105+// ValidateSession validates the AccessToken
106+func (p *SourceHutProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
107+ return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
108+}
providers/srht_test.gonew file+77
@@ -0,0 +1,77 @@
1+package providers
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+)
12+
13+func testSourceHutProvider(hostname string) *SourceHutProvider {
14+ p := NewSourceHutProvider(
15+ &ProviderData{
16+ ProviderName: "SourceHut",
17+ LoginURL: &url.URL{},
18+ RedeemURL: &url.URL{},
19+ ProfileURL: &url.URL{},
20+ ValidateURL: &url.URL{},
21+ Scope: ""},
22+ )
23+ p.ProviderName = "SourceHut"
24+
25+ if hostname != "" {
26+ updateURL(p.Data().LoginURL, hostname)
27+ updateURL(p.Data().RedeemURL, hostname)
28+ updateURL(p.Data().ProfileURL, hostname)
29+ updateURL(p.Data().ValidateURL, hostname)
30+ }
31+ return p
32+}
33+
34+func testSourceHutBackend(payloads map[string][]string) *httptest.Server {
35+ return httptest.NewServer(http.HandlerFunc(
36+ func(w http.ResponseWriter, r *http.Request) {
37+ index := 0
38+ payload, ok := payloads[r.URL.Path]
39+ if !ok {
40+ w.WriteHeader(404)
41+ } else if payload[index] == "" {
42+ w.WriteHeader(204)
43+ } else {
44+ w.WriteHeader(200)
45+ w.Write([]byte(payload[index]))
46+ }
47+ }))
48+}
49+
50+func TestSourceHutProvider_ValidateSessionWithBaseUrl(t *testing.T) {
51+ b := testSourceHutBackend(map[string][]string{})
52+ defer b.Close()
53+
54+ bURL, _ := url.Parse(b.URL)
55+ p := testSourceHutProvider(bURL.Host)
56+
57+ session := CreateAuthorizedSession()
58+
59+ valid := p.ValidateSession(context.Background(), session)
60+ assert.False(t, valid)
61+}
62+
63+func TestSourceHutProvider_ValidateSessionWithUserEmails(t *testing.T) {
64+ b := testSourceHutBackend(map[string][]string{
65+ "/query": {`{"data":{"me":{"username":"bitfehler","email":"ch@bitfehler.net"}}}`},
66+ "/profile": {`ok`},
67+ })
68+ defer b.Close()
69+
70+ bURL, _ := url.Parse(b.URL)
71+ p := testSourceHutProvider(bURL.Host)
72+
73+ session := CreateAuthorizedSession()
74+
75+ valid := p.ValidateSession(context.Background(), session)
76+ assert.True(t, valid)
77+}