You've already forked oauth2-proxy
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:
@ -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
|
||||
|
||||
|
25
docs/docs/configuration/providers/sourcehut.md
Normal file
25
docs/docs/configuration/providers/sourcehut.md
Normal 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).
|
||||
|
@ -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 {
|
||||
|
@ -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
108
providers/srht.go
Normal 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
77
providers/srht_test.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user