Skip to content

Add SourceHut (sr.ht) provider #2359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- [#2357](https://github.com/oauth2-proxy/oauth2-proxy/pull/2357) Update ojg to latest release (@bitfehler)
- [#1922](https://github.com/oauth2-proxy/oauth2-proxy/pull/1922) Added support for env variables in the alpha struct (@hevans-dglcom)
- [#2235](https://github.com/oauth2-proxy/oauth2-proxy/pull/2235) Bump golang to 1.21 and min allowed version to 1.20 (@tuunit)
- [#2359](https://github.com/oauth2-proxy/oauth2-proxy/pull/2359) Add SourceHut (sr.ht) provider(@bitfehler)

# V7.5.1

Expand Down
25 changes: 25 additions & 0 deletions docs/docs/configuration/providers/sourcehut.md
Original file line number Diff line number Diff line change
@@ -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).

3 changes: 3 additions & 0 deletions pkg/apis/options/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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 {
Expand Down
5 changes: 4 additions & 1 deletion providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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)
}
Expand Down Expand Up @@ -179,7 +181,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:
return true, nil
Expand Down
108 changes: 108 additions & 0 deletions providers/srht.go
Original file line number Diff line number Diff line change
@@ -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 changes: 77 additions & 0 deletions providers/srht_test.go
Original file line number Diff line number Diff line change
@@ -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":"[email protected]"}}}`},
"/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)
}
Loading