Add Nextcloud provider (#179)

This commit is contained in:
Casey Link 2019-06-05 18:59:04 +02:00 committed by Casey Link
parent 82a3d5afdc
commit b40d2244a8
6 changed files with 218 additions and 0 deletions

4
.github/CODEOWNERS vendored
View File

@ -14,3 +14,7 @@ providers/logingov_test.go @timothy-spencer
# Bitbucket provider
providers/bitbucket.go @aledeganopix4d
providers/bitbucket_test.go @aledeganopix4d
# Nextcloud provider
providers/nextcloud.go @Ramblurr
providers/nextcloud_test.go @Ramblurr

View File

@ -2,6 +2,8 @@
## Changes since v4.0.0
- [#179](https://github.com/pusher/oauth2_proxy/pull/179) Add Nextcloud provider
# v4.0.0
## Release Highlights

View File

@ -18,6 +18,7 @@ Valid providers are :
- [GitLab](#gitlab-auth-provider)
- [LinkedIn](#linkedin-auth-provider)
- [login.gov](#logingov-provider)
- [Nextcloud](#nextcloud-provider)
The provider can be selected using the `provider` configuration value.
@ -271,6 +272,32 @@ In this case, you can set the `-skip-oidc-discovery` option, and supply those re
-email-domain example.com
```
### Nextcloud Provider
The Nextcloud provider allows you to authenticate against users in your
Nextcloud instance.
When you are using the Nextcloud provider, you must specify the urls via
configuration, environment variable, or command line argument. Depending
on whether your Nextcloud instance is using pretty urls your urls may be of the
form `/index.php/apps/oauth2/*` or `/apps/oauth2/*`.
Refer to the [OAuth2
documentation](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/oauth2.html)
to setup the client id and client secret. Your "Redirection URI" will be
`https://internalapp.yourcompany.com/oauth2/callback`.
```
-provider nextcloud
-client-id <from nextcloud admin>
-client-secret <from nextcloud admin>
-login-url="<your gitlab url>/index.php/apps/oauth2/authorize"
-redeem-url="<your gitlab url>/index.php/apps/oauth2/api/v1/token"
-validate-url="<your gitlab url>/ocs/v2.php/cloud/user?format=json"
```
Note: in *all* cases the validate-url will *not* have the `index.php`.
## Email Authentication
To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`.

45
providers/nextcloud.go Normal file
View File

@ -0,0 +1,45 @@
package providers
import (
"fmt"
"net/http"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/logger"
"github.com/pusher/oauth2_proxy/pkg/requests"
)
// NextcloudProvider represents an Nextcloud based Identity Provider
type NextcloudProvider struct {
*ProviderData
}
// NewNextcloudProvider initiates a new NextcloudProvider
func NewNextcloudProvider(p *ProviderData) *NextcloudProvider {
p.ProviderName = "Nextcloud"
return &NextcloudProvider{ProviderData: p}
}
func getNextcloudHeader(accessToken string) http.Header {
header := make(http.Header)
header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
return header
}
// GetEmailAddress returns the Account email address
func (p *NextcloudProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
req, err := http.NewRequest("GET",
p.ValidateURL.String(), nil)
if err != nil {
logger.Printf("failed building request %s", err)
return "", err
}
req.Header = getNextcloudHeader(s.AccessToken)
json, err := requests.Request(req)
if err != nil {
logger.Printf("failed making request %s", err)
return "", err
}
email, err := json.Get("ocs").Get("data").Get("email").String()
return email, err
}

138
providers/nextcloud_test.go Normal file
View File

@ -0,0 +1,138 @@
package providers
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/stretchr/testify/assert"
)
const formatJSON = "format=json"
const userPath = "/ocs/v2.php/cloud/user"
func testNextcloudProvider(hostname string) *NextcloudProvider {
p := NewNextcloudProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})
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 testNextcloudBackend(payload string) *httptest.Server {
path := userPath
query := formatJSON
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path || r.URL.RawQuery != query {
w.WriteHeader(404)
} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token_nextcloud" {
w.WriteHeader(403)
} else {
w.WriteHeader(200)
w.Write([]byte(payload))
}
}))
}
func TestNextcloudProviderDefaults(t *testing.T) {
p := testNextcloudProvider("")
assert.NotEqual(t, nil, p)
assert.Equal(t, "Nextcloud", p.Data().ProviderName)
assert.Equal(t, "",
p.Data().LoginURL.String())
assert.Equal(t, "",
p.Data().RedeemURL.String())
assert.Equal(t, "",
p.Data().ValidateURL.String())
}
func TestNextcloudProviderOverrides(t *testing.T) {
p := NewNextcloudProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/index.php/apps/oauth2/authorize"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/index.php/apps/oauth2/api/v1/token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/test/ocs/v2.php/cloud/user",
RawQuery: formatJSON},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "Nextcloud", p.Data().ProviderName)
assert.Equal(t, "https://example.com/index.php/apps/oauth2/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/index.php/apps/oauth2/api/v1/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/test/ocs/v2.php/cloud/user?"+formatJSON,
p.Data().ValidateURL.String())
}
func TestNextcloudProviderGetEmailAddress(t *testing.T) {
b := testNextcloudBackend("{\"ocs\": {\"data\": { \"email\": \"michael.bland@gsa.gov\"}}}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testNextcloudProvider(bURL.Host)
p.ValidateURL.Path = userPath
p.ValidateURL.RawQuery = formatJSON
session := &sessions.SessionState{AccessToken: "imaginary_access_token_nextcloud"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}
// Note that trying to trigger the "failed building request" case is not
// practical, since the only way it can fail is if the URL fails to parse.
func TestNextcloudProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testNextcloudBackend("unused payload")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testNextcloudProvider(bURL.Host)
p.ValidateURL.Path = userPath
p.ValidateURL.RawQuery = formatJSON
// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
// JSON to fail.
session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}
func TestNextcloudProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testNextcloudBackend("{\"foo\": \"bar\"}")
defer b.Close()
bURL, _ := url.Parse(b.URL)
p := testNextcloudProvider(bURL.Host)
p.ValidateURL.Path = userPath
p.ValidateURL.RawQuery = formatJSON
session := &sessions.SessionState{AccessToken: "imaginary_access_token_nextcloud"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}

View File

@ -38,6 +38,8 @@ func New(provider string, p *ProviderData) Provider {
return NewLoginGovProvider(p)
case "bitbucket":
return NewBitbucketProvider(p)
case "nextcloud":
return NewNextcloudProvider(p)
default:
return NewGoogleProvider(p)
}