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)
|
- [#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)
|
- [#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)
|
- [#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
|
# 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 is the provider type for OIDC
|
||||||
OIDCProvider ProviderType = "oidc"
|
OIDCProvider ProviderType = "oidc"
|
||||||
|
|
||||||
|
// SourceHutProvider is the provider type for SourceHut
|
||||||
|
SourceHutProvider ProviderType = "sourcehut"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeycloakOptions struct {
|
type KeycloakOptions struct {
|
||||||
|
@ -67,6 +67,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
|
|||||||
return NewNextcloudProvider(providerData), nil
|
return NewNextcloudProvider(providerData), nil
|
||||||
case options.OIDCProvider:
|
case options.OIDCProvider:
|
||||||
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
|
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
|
||||||
|
case options.SourceHutProvider:
|
||||||
|
return NewSourceHutProvider(providerData), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown provider type %q", providerConfig.Type)
|
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) {
|
func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
|
||||||
switch providerType {
|
switch providerType {
|
||||||
case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
|
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
|
return false, nil
|
||||||
case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider, options.MicrosoftEntraIDProvider:
|
case options.ADFSProvider, options.AzureProvider, options.GitLabProvider, options.KeycloakOIDCProvider, options.OIDCProvider, options.MicrosoftEntraIDProvider:
|
||||||
return true, nil
|
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