1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-08-06 22:42:56 +02:00

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>
This commit is contained in:
Conrad Hoffmann
2025-07-22 08:16:32 +02:00
committed by GitHub
parent 4d17bc1d68
commit a88306be98
6 changed files with 218 additions and 1 deletions

View File

@ -11,6 +11,7 @@
- [#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)
- [#2605](https://github.com/oauth2-proxy/oauth2-proxy/pull/2605) fix: show login page on broken cookie (@Primexz)
- [#2743](https://github.com/oauth2-proxy/oauth2-proxy/pull/2743) feat: allow use more possible google admin-sdk api scopes (@BobDu)
- [#2359](https://github.com/oauth2-proxy/oauth2-proxy/pull/2359) feat: add SourceHut (sr.ht) provider(@bitfehler)
# V7.10.0

View File

@ -0,0 +1,25 @@
---
id: sourcehut
title: SourceHut
---
1. Create a new OAuth client: https://meta.sr.ht/oauth2
2. Under `Redirection URI` enter the correct URL, i.e.
`https://internal.yourcompany.com/oauth2/callback`
To use the provider, start with `--provider=sourcehut`.
If you are hosting your own SourceHut instance, make sure you set the following
to the appropriate URLs:
```shell
--login-url="https://<meta.your.instance>/oauth2/authorize"
--redeem-url="https://<meta.your.instance>/oauth2/access-token"
--profile-url="https://<meta.your.instance>/query"
--validate-url="https://<meta.your.instance>/profile"
```
The default configuration allows everyone with an account to authenticate.
Restricting access is currently only supported by
[email](#email-authentication).

View File

@ -147,6 +147,9 @@ const (
// OIDCProvider is the provider type for OIDC
OIDCProvider ProviderType = "oidc"
// SourceHutProvider is the provider type for SourceHut
SourceHutProvider ProviderType = "sourcehut"
)
type KeycloakOptions struct {

View File

@ -67,6 +67,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
return NewNextcloudProvider(providerData), nil
case options.OIDCProvider:
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
case options.SourceHutProvider:
return NewSourceHutProvider(providerData), nil
default:
return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
}
@ -183,7 +185,8 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
switch providerType {
case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
options.NextCloudProvider, options.SourceHutProvider:
return false, nil
case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider, options.MicrosoftEntraIDProvider:
return true, nil

108
providers/srht.go Normal file
View File

@ -0,0 +1,108 @@
package providers
import (
"bytes"
"context"
"fmt"
"net/url"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
)
type SourceHutProvider struct {
*ProviderData
}
var _ Provider = (*SourceHutProvider)(nil)
const (
SourceHutProviderName = "SourceHut"
SourceHutDefaultScope = "meta.sr.ht/PROFILE:RO"
)
var (
// Default Login URL for SourceHut.
// Pre-parsed URL of https://meta.sr.ht/oauth2/authorize.
SourceHutDefaultLoginURL = &url.URL{
Scheme: "https",
Host: "meta.sr.ht",
Path: "/oauth2/authorize",
}
// Default Redeem URL for SourceHut.
// Pre-parsed URL of https://meta.sr.ht/oauth2/access-token.
SourceHutDefaultRedeemURL = &url.URL{
Scheme: "https",
Host: "meta.sr.ht",
Path: "/oauth2/access-token",
}
// Default Profile URL for SourceHut.
// Pre-parsed URL of https://meta.sr.ht/query.
SourceHutDefaultProfileURL = &url.URL{
Scheme: "https",
Host: "meta.sr.ht",
Path: "/query",
}
// Default Validation URL for SourceHut.
// Pre-parsed URL of https://meta.sr.ht/profile.
SourceHutDefaultValidateURL = &url.URL{
Scheme: "https",
Host: "meta.sr.ht",
Path: "/profile",
}
)
// NewSourceHutProvider creates a SourceHutProvider using the passed ProviderData
func NewSourceHutProvider(p *ProviderData) *SourceHutProvider {
p.setProviderDefaults(providerDefaults{
name: SourceHutProviderName,
loginURL: SourceHutDefaultLoginURL,
redeemURL: SourceHutDefaultRedeemURL,
profileURL: SourceHutDefaultProfileURL,
validateURL: SourceHutDefaultValidateURL,
scope: SourceHutDefaultScope,
})
return &SourceHutProvider{ProviderData: p}
}
// EnrichSession uses the SourceHut userinfo endpoint to populate the session's
// email and username.
func (p *SourceHutProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
json, err := requests.New(p.ProfileURL.String()).
WithContext(ctx).
WithMethod("POST").
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+s.AccessToken).
WithBody(bytes.NewBufferString(`{"query": "{ me { username, email } }"}`)).
Do().
UnmarshalSimpleJSON()
if err != nil {
logger.Errorf("failed making request %v", err)
return err
}
email, err := json.GetPath("data", "me", "email").String()
if err != nil {
return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err)
}
s.Email = email
username, err := json.GetPath("data", "me", "username").String()
if err != nil {
return fmt.Errorf("unable to extract username from userinfo endpoint: %v", err)
}
s.PreferredUsername = username
s.User = username
return nil
}
// ValidateSession validates the AccessToken
func (p *SourceHutProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
}

77
providers/srht_test.go Normal file
View File

@ -0,0 +1,77 @@
package providers
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func testSourceHutProvider(hostname string) *SourceHutProvider {
p := NewSourceHutProvider(
&ProviderData{
ProviderName: "SourceHut",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""},
)
p.ProviderName = "SourceHut"
if hostname != "" {
updateURL(p.Data().LoginURL, hostname)
updateURL(p.Data().RedeemURL, hostname)
updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname)
}
return p
}
func testSourceHutBackend(payloads map[string][]string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
index := 0
payload, ok := payloads[r.URL.Path]
if !ok {
w.WriteHeader(404)
} else if payload[index] == "" {
w.WriteHeader(204)
} else {
w.WriteHeader(200)
w.Write([]byte(payload[index]))
}
}))
}
func TestSourceHutProvider_ValidateSessionWithBaseUrl(t *testing.T) {
b := testSourceHutBackend(map[string][]string{})
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testSourceHutProvider(bURL.Host)
session := CreateAuthorizedSession()
valid := p.ValidateSession(context.Background(), session)
assert.False(t, valid)
}
func TestSourceHutProvider_ValidateSessionWithUserEmails(t *testing.T) {
b := testSourceHutBackend(map[string][]string{
"/query": {`{"data":{"me":{"username":"bitfehler","email":"ch@bitfehler.net"}}}`},
"/profile": {`ok`},
})
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testSourceHutProvider(bURL.Host)
session := CreateAuthorizedSession()
valid := p.ValidateSession(context.Background(), session)
assert.True(t, valid)
}