Skip to content

Commit c7db32a

Browse files
bitfehlertuunit
andcommitted
Add SourceHut (sr.ht) provider
Co-authored-by: Jan Larwig <[email protected]>
1 parent e293ddd commit c7db32a

File tree

6 files changed

+218
-1
lines changed

6 files changed

+218
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
- [#2357](https://github.com/oauth2-proxy/oauth2-proxy/pull/2357) Update ojg to latest release (@bitfehler)
5454
- [#1922](https://github.com/oauth2-proxy/oauth2-proxy/pull/1922) Added support for env variables in the alpha struct (@hevans-dglcom)
5555
- [#2235](https://github.com/oauth2-proxy/oauth2-proxy/pull/2235) Bump golang to 1.21 and min allowed version to 1.20 (@tuunit)
56+
- [#2359](https://github.com/oauth2-proxy/oauth2-proxy/pull/2359) Add SourceHut (sr.ht) provider(@bitfehler)
5657

5758
# V7.5.1
5859

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ const (
136136

137137
// OIDCProvider is the provider type for OIDC
138138
OIDCProvider ProviderType = "oidc"
139+
140+
// SourceHutProvider is the provider type for SourceHut
141+
SourceHutProvider ProviderType = "sourcehut"
139142
)
140143

141144
type KeycloakOptions struct {

providers/providers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
6565
return NewNextcloudProvider(providerData), nil
6666
case options.OIDCProvider:
6767
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
68+
case options.SourceHutProvider:
69+
return NewSourceHutProvider(providerData), nil
6870
default:
6971
return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
7072
}
@@ -179,7 +181,8 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
179181
func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
180182
switch providerType {
181183
case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
182-
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider:
184+
options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
185+
options.NextCloudProvider, options.SourceHutProvider:
183186
return false, nil
184187
case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider:
185188
return true, nil

providers/srht.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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":"[email protected]"}}}`},
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+
}

0 commit comments

Comments
 (0)