Skip to content

Commit 9e5994b

Browse files
authored
Add Generic OAuth Provider (#138)
1 parent 870724c commit 9e5994b

File tree

7 files changed

+285
-4
lines changed

7 files changed

+285
-4
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ Also in the examples directory is [docker-compose-auth-host.yml](https://github.
9898
9999
#### Provider Setup
100100
101+
Below are some general notes on provider setup, specific instructions and examples for a number of providers can be found on the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page.
102+
101103
##### Google
102104
103105
Head to https://console.developers.google.com and make sure you've switched to the correct email account.
@@ -114,6 +116,25 @@ Any provider that supports OpenID Connect 1.0 can be configured via the OIDC con
114116

115117
You must set the `providers.oidc.issuer-url`, `providers.oidc.client-id` and `providers.oidc.client-secret` config options.
116118

119+
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
120+
121+
##### Generic OAuth2
122+
123+
For providers that don't support OpenID Connect, we also have the Generic OAuth2 provider where you can statically configure the OAuth2 and "user" endpoints.
124+
125+
You must set:
126+
- `providers.generic-oauth.auth-url` - URL the client should be sent to authenticate the authenticate
127+
- `providers.generic-oauth.token-url` - URL the service should call to exchange an auth code for an access token
128+
- `providers.generic-oauth.user-url` - URL used to retrieve user info (service makes a GET request)
129+
- `providers.generic-oauth.client-id` - Client ID
130+
- `providers.generic-oauth.client-secret` - Client Secret
131+
132+
You can also set:
133+
- `providers.generic-oauth.scope`- Any scopes that should be included in the request (default: profile, email)
134+
- `providers.generic-oauth.token-style` - How token is presented when querying the User URL. Can be `header` or `query`, defaults to `header`. With `header` the token is provided in an Authorization header, with query the token is provided in the `access_token` query string value.
135+
136+
Please see the [Provider Setup](https://github.com/thomseddon/traefik-forward-auth/wiki/Provider-Setup) wiki page for examples.
137+
117138
## Configuration
118139

119140
### Overview
@@ -134,7 +155,7 @@ Application Options:
134155
--cookie-name= Cookie Name (default: _forward_auth) [$COOKIE_NAME]
135156
--csrf-cookie-name= CSRF Cookie Name (default: _forward_auth_csrf) [$CSRF_COOKIE_NAME]
136157
--default-action=[auth|allow] Default action (default: auth) [$DEFAULT_ACTION]
137-
--default-provider=[google|oidc] Default provider (default: google) [$DEFAULT_PROVIDER]
158+
--default-provider=[google|oidc|generic-oauth] Default provider (default: google) [$DEFAULT_PROVIDER]
138159
--domain= Only allow given email domains, can be set multiple times [$DOMAIN]
139160
--lifetime= Lifetime in seconds (default: 43200) [$LIFETIME]
140161
--logout-redirect= URL to redirect to following logout [$LOGOUT_REDIRECT]
@@ -154,6 +175,17 @@ OIDC Provider:
154175
--providers.oidc.client-secret= Client Secret [$PROVIDERS_OIDC_CLIENT_SECRET]
155176
--providers.oidc.resource= Optional resource indicator [$PROVIDERS_OIDC_RESOURCE]
156177
178+
Generic OAuth2 Provider:
179+
--providers.generic-oauth.auth-url= Auth/Login URL [$PROVIDERS_GENERIC_OAUTH_AUTH_URL]
180+
--providers.generic-oauth.token-url= Token URL [$PROVIDERS_GENERIC_OAUTH_TOKEN_URL]
181+
--providers.generic-oauth.user-url= URL used to retrieve user info [$PROVIDERS_GENERIC_OAUTH_USER_URL]
182+
--providers.generic-oauth.client-id= Client ID [$PROVIDERS_GENERIC_OAUTH_CLIENT_ID]
183+
--providers.generic-oauth.client-secret= Client Secret [$PROVIDERS_GENERIC_OAUTH_CLIENT_SECRET]
184+
--providers.generic-oauth.scope= Scopes (default: profile, email) [$PROVIDERS_GENERIC_OAUTH_SCOPE]
185+
--providers.generic-oauth.token-style=[header|query] How token is presented when querying the User URL (default: header)
186+
[$PROVIDERS_GENERIC_OAUTH_TOKEN_STYLE]
187+
--providers.generic-oauth.resource= Optional resource indicator [$PROVIDERS_GENERIC_OAUTH_RESOURCE]
188+
157189
Help Options:
158190
-h, --help Show this help message
159191
```

internal/auth_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,11 @@ func TestMakeState(t *testing.T) {
316316
p2 := provider.OIDC{}
317317
state = MakeState(r, &p2, "nonce")
318318
assert.Equal("nonce:oidc:http://example.com/hello", state)
319+
320+
// Test with Generic OAuth
321+
p3 := provider.GenericOAuth{}
322+
state = MakeState(r, &p3, "nonce")
323+
assert.Equal("nonce:generic-oauth:http://example.com/hello", state)
319324
}
320325

321326
func TestAuthNonce(t *testing.T) {

internal/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type Config struct {
3131
CookieName string `long:"cookie-name" env:"COOKIE_NAME" default:"_forward_auth" description:"Cookie Name"`
3232
CSRFCookieName string `long:"csrf-cookie-name" env:"CSRF_COOKIE_NAME" default:"_forward_auth_csrf" description:"CSRF Cookie Name"`
3333
DefaultAction string `long:"default-action" env:"DEFAULT_ACTION" default:"auth" choice:"auth" choice:"allow" description:"Default action"`
34-
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" description:"Default provider"`
34+
DefaultProvider string `long:"default-provider" env:"DEFAULT_PROVIDER" default:"google" choice:"google" choice:"oidc" choice:"generic-oauth" description:"Default provider"`
3535
Domains CommaSeparatedList `long:"domain" env:"DOMAIN" env-delim:"," description:"Only allow given email domains, can be set multiple times"`
3636
LifetimeString int `long:"lifetime" env:"LIFETIME" default:"43200" description:"Lifetime in seconds"`
3737
LogoutRedirect string `long:"logout-redirect" env:"LOGOUT_REDIRECT" description:"URL to redirect to following logout"`
@@ -275,6 +275,8 @@ func (c *Config) GetProvider(name string) (provider.Provider, error) {
275275
return &c.Providers.Google, nil
276276
case "oidc":
277277
return &c.Providers.OIDC, nil
278+
case "generic-oauth":
279+
return &c.Providers.GenericOAuth, nil
278280
}
279281

280282
return nil, fmt.Errorf("Unknown provider: %s", name)

internal/config_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@ func TestConfigGetProvider(t *testing.T) {
366366
assert.Nil(err)
367367
assert.Equal(&c.Providers.OIDC, p)
368368

369+
// Should be able to get "generic-oauth" provider
370+
p, err = c.GetProvider("generic-oauth")
371+
assert.Nil(err)
372+
assert.Equal(&c.Providers.GenericOAuth, p)
373+
369374
// Should catch unknown provider
370375
p, err = c.GetProvider("bad")
371376
if assert.Error(err) {

internal/provider/generic_oauth.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
10+
"golang.org/x/oauth2"
11+
)
12+
13+
// GenericOAuth provider
14+
type GenericOAuth struct {
15+
AuthURL string `long:"auth-url" env:"AUTH_URL" description:"Auth/Login URL"`
16+
TokenURL string `long:"token-url" env:"TOKEN_URL" description:"Token URL"`
17+
UserURL string `long:"user-url" env:"USER_URL" description:"URL used to retrieve user info"`
18+
ClientID string `long:"client-id" env:"CLIENT_ID" description:"Client ID"`
19+
ClientSecret string `long:"client-secret" env:"CLIENT_SECRET" description:"Client Secret" json:"-"`
20+
Scopes []string `long:"scope" env:"SCOPE" env-delim:"," default:"profile" default:"email" description:"Scopes"`
21+
TokenStyle string `long:"token-style" env:"TOKEN_STYLE" default:"header" choice:"header" choice:"query" description:"How token is presented when querying the User URL"`
22+
23+
OAuthProvider
24+
}
25+
26+
// Name returns the name of the provider
27+
func (o *GenericOAuth) Name() string {
28+
return "generic-oauth"
29+
}
30+
31+
// Setup performs validation and setup
32+
func (o *GenericOAuth) Setup() error {
33+
// Check parmas
34+
if o.AuthURL == "" || o.TokenURL == "" || o.UserURL == "" || o.ClientID == "" || o.ClientSecret == "" {
35+
return errors.New("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set")
36+
}
37+
38+
// Create oauth2 config
39+
o.Config = &oauth2.Config{
40+
ClientID: o.ClientID,
41+
ClientSecret: o.ClientSecret,
42+
Endpoint: oauth2.Endpoint{
43+
AuthURL: o.AuthURL,
44+
TokenURL: o.TokenURL,
45+
},
46+
Scopes: o.Scopes,
47+
}
48+
49+
o.ctx = context.Background()
50+
51+
return nil
52+
}
53+
54+
// GetLoginURL provides the login url for the given redirect uri and state
55+
func (o *GenericOAuth) GetLoginURL(redirectURI, state string) string {
56+
return o.OAuthGetLoginURL(redirectURI, state)
57+
}
58+
59+
// ExchangeCode exchanges the given redirect uri and code for a token
60+
func (o *GenericOAuth) ExchangeCode(redirectURI, code string) (string, error) {
61+
token, err := o.OAuthExchangeCode(redirectURI, code)
62+
if err != nil {
63+
return "", err
64+
}
65+
66+
return token.AccessToken, nil
67+
}
68+
69+
// GetUser uses the given token and returns a complete provider.User object
70+
func (o *GenericOAuth) GetUser(token string) (User, error) {
71+
var user User
72+
73+
req, err := http.NewRequest("GET", o.UserURL, nil)
74+
if err != nil {
75+
return user, err
76+
}
77+
78+
if o.TokenStyle == "header" {
79+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
80+
} else if o.TokenStyle == "query" {
81+
q := req.URL.Query()
82+
q.Add("access_token", token)
83+
req.URL.RawQuery = q.Encode()
84+
}
85+
86+
client := &http.Client{}
87+
res, err := client.Do(req)
88+
if err != nil {
89+
return user, err
90+
}
91+
92+
defer res.Body.Close()
93+
err = json.NewDecoder(res.Body).Decode(&user)
94+
95+
return user, err
96+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package provider
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"golang.org/x/oauth2"
9+
)
10+
11+
// Tests
12+
13+
func TestGenericOAuthName(t *testing.T) {
14+
p := GenericOAuth{}
15+
assert.Equal(t, "generic-oauth", p.Name())
16+
}
17+
18+
func TestGenericOAuthSetup(t *testing.T) {
19+
assert := assert.New(t)
20+
p := GenericOAuth{}
21+
22+
// Check validation
23+
err := p.Setup()
24+
if assert.Error(err) {
25+
assert.Equal("providers.generic-oauth.auth-url, providers.generic-oauth.token-url, providers.generic-oauth.user-url, providers.generic-oauth.client-id, providers.generic-oauth.client-secret must be set", err.Error())
26+
}
27+
28+
// Check setup
29+
p = GenericOAuth{
30+
AuthURL: "https://provider.com/oauth2/auth",
31+
TokenURL: "https://provider.com/oauth2/token",
32+
UserURL: "https://provider.com/oauth2/user",
33+
ClientID: "id",
34+
ClientSecret: "secret",
35+
}
36+
err = p.Setup()
37+
assert.Nil(err)
38+
}
39+
40+
func TestGenericOAuthGetLoginURL(t *testing.T) {
41+
assert := assert.New(t)
42+
p := GenericOAuth{
43+
AuthURL: "https://provider.com/oauth2/auth",
44+
TokenURL: "https://provider.com/oauth2/token",
45+
UserURL: "https://provider.com/oauth2/user",
46+
ClientID: "idtest",
47+
ClientSecret: "secret",
48+
Scopes: []string{"scopetest"},
49+
}
50+
err := p.Setup()
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
55+
// Check url
56+
uri, err := url.Parse(p.GetLoginURL("http://example.com/_oauth", "state"))
57+
assert.Nil(err)
58+
assert.Equal("https", uri.Scheme)
59+
assert.Equal("provider.com", uri.Host)
60+
assert.Equal("/oauth2/auth", uri.Path)
61+
62+
// Check query string
63+
qs := uri.Query()
64+
expectedQs := url.Values{
65+
"client_id": []string{"idtest"},
66+
"redirect_uri": []string{"http://example.com/_oauth"},
67+
"response_type": []string{"code"},
68+
"scope": []string{"scopetest"},
69+
"state": []string{"state"},
70+
}
71+
assert.Equal(expectedQs, qs)
72+
}
73+
74+
func TestGenericOAuthExchangeCode(t *testing.T) {
75+
assert := assert.New(t)
76+
77+
// Setup server
78+
expected := url.Values{
79+
"client_id": []string{"idtest"},
80+
"client_secret": []string{"sectest"},
81+
"code": []string{"code"},
82+
"grant_type": []string{"authorization_code"},
83+
"redirect_uri": []string{"http://example.com/_oauth"},
84+
}
85+
server, serverURL := NewOAuthServer(t, map[string]string{
86+
"token": expected.Encode(),
87+
})
88+
defer server.Close()
89+
90+
// Setup provider
91+
p := GenericOAuth{
92+
AuthURL: "https://provider.com/oauth2/auth",
93+
TokenURL: serverURL.String() + "/token",
94+
UserURL: "https://provider.com/oauth2/user",
95+
ClientID: "idtest",
96+
ClientSecret: "sectest",
97+
}
98+
err := p.Setup()
99+
if err != nil {
100+
t.Fatal(err)
101+
}
102+
103+
// We force AuthStyleInParams to prevent the test failure when the
104+
// AuthStyleInHeader is attempted
105+
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
106+
107+
token, err := p.ExchangeCode("http://example.com/_oauth", "code")
108+
assert.Nil(err)
109+
assert.Equal("123456789", token)
110+
}
111+
112+
func TestGenericOAuthGetUser(t *testing.T) {
113+
assert := assert.New(t)
114+
115+
// Setup server
116+
server, serverURL := NewOAuthServer(t, nil)
117+
defer server.Close()
118+
119+
// Setup provider
120+
p := GenericOAuth{
121+
AuthURL: "https://provider.com/oauth2/auth",
122+
TokenURL: "https://provider.com/oauth2/token",
123+
UserURL: serverURL.String() + "/userinfo",
124+
ClientID: "idtest",
125+
ClientSecret: "sectest",
126+
}
127+
err := p.Setup()
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
132+
// We force AuthStyleInParams to prevent the test failure when the
133+
// AuthStyleInHeader is attempted
134+
p.Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
135+
136+
user, err := p.GetUser("123456789")
137+
assert.Nil(err)
138+
139+
assert.Equal("example@example.com", user.Email)
140+
}

internal/provider/providers.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import (
99

1010
// Providers contains all the implemented providers
1111
type Providers struct {
12-
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
13-
OIDC OIDC `group:"OIDC Provider" namespace:"oidc" env-namespace:"OIDC"`
12+
Google Google `group:"Google Provider" namespace:"google" env-namespace:"GOOGLE"`
13+
OIDC OIDC `group:"OIDC Provider" namespace:"oidc" env-namespace:"OIDC"`
14+
GenericOAuth GenericOAuth `group:"Generic OAuth2 Provider" namespace:"generic-oauth" env-namespace:"GENERIC_OAUTH"`
1415
}
1516

1617
// Provider is used to authenticate users

0 commit comments

Comments
 (0)