git.iko.soy/typeable/oauth2-proxy
on master help sign in
Added ADFS Provider
commita14c0c212179b42a6f0d01174b66e876c391da91 authorSami Racho <sami.racho@adidas.com> dateSun 13 Jun 2021, 10:00 (5 years ago) parent381ac91
8 files changed+347−2
CHANGELOG.md+1 docs/docs/configuration/alpha_config.md+12−1 docs/docs/configuration/auth.md+16 pkg/apis/options/providers.go+9−1 pkg/validation/options.go+2 providers/adfs.go+100 providers/adfs_test.go+205 providers/providers.go+2
CHANGELOG.md+1
@@ -7,6 +7,7 @@
77## Breaking Changes
88
99## Changes since v7.1.3
10+- [#1238](https://github.com/oauth2-proxy/oauth2-proxy/pull/1238) Added ADFS provider (@samirachoadi)
1011- [#1227](https://github.com/oauth2-proxy/oauth2-proxy/pull/1227) Fix Refresh Session not working for multiple cookies (@rishi1111)
1112- [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7)
1213- [#1108](https://github.com/oauth2-proxy/oauth2-proxy/pull/1108) Add alternative ways to generate cookie secrets to docs (@JoelSpeed)
docs/docs/configuration/alpha_config.md+12−1
@@ -101,6 +101,16 @@ You must remove these options before starting OAuth2 Proxy with `--alpha-config`
101101## Configuration Reference
102102<!--- THIS FILE IS AUTOGENERATED!!! DO NOT EDIT!!! -->
103103
104+### ADFSOptions
105+
106+(**Appears on:** [Provider](#provider))
107+
108+
109+
110+| Field | Type | Description |
111+| ----- | ---- | ----------- |
112+| `skipScope` | _bool_ | Skip adding the scope parameter in login request<br/>Default value is 'false' |
113+
104114### AlphaOptions
105115
106116AlphaOptions contains alpha structured configuration options.
@@ -284,6 +294,7 @@ Provider holds all configuration for a single provider
284294| `clientSecretFile` | _string_ | ClientSecretFile is the name of the file<br/>containing the OAuth Client Secret, it will be used if ClientSecret is not set. |
285295| `keycloakConfig` | _[KeycloakOptions](#keycloakoptions)_ | KeycloakConfig holds all configurations for Keycloak provider. |
286296| `azureConfig` | _[AzureOptions](#azureoptions)_ | AzureConfig holds all configurations for Azure provider. |
297+| `ADFSConfig` | _[ADFSOptions](#adfsoptions)_ | ADFSConfig holds all configurations for ADFS provider. |
287298| `bitbucketConfig` | _[BitbucketOptions](#bitbucketoptions)_ | BitbucketConfig holds all configurations for Bitbucket provider. |
288299| `githubConfig` | _[GitHubOptions](#githuboptions)_ | GitHubConfig holds all configurations for GitHubC provider. |
289300| `gitlabConfig` | _[GitLabOptions](#gitlaboptions)_ | GitLabConfig holds all configurations for GitLab provider. |
@@ -297,7 +308,7 @@ Provider holds all configuration for a single provider
297308| `loginURL` | _string_ | LoginURL is the authentication endpoint |
298309| `redeemURL` | _string_ | RedeemURL is the token redemption endpoint |
299310| `profileURL` | _string_ | ProfileURL is the profile access endpoint |
300−| `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD only) |
311+| `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD and ADFS only) |
301312| `validateURL` | _string_ | ValidateURL is the access token validation endpoint |
302313| `scope` | _string_ | Scope is the OAuth scope specification |
303314| `prompt` | _string_ | Prompt is OIDC prompt |
docs/docs/configuration/auth.md+16
@@ -9,6 +9,7 @@ Valid providers are :
99
1010- [Google](#google-auth-provider) _default_
1111- [Azure](#azure-auth-provider)
12+- [ADFS](#adfs-auth-provider)
1213- [Facebook](#facebook-auth-provider)
1314- [GitHub](#github-auth-provider)
1415- [Keycloak](#keycloak-auth-provider)
@@ -88,6 +89,21 @@ Note: The user is checked against the group members list on initial authenticati
8889
8990Note: When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](sessions.md#redis-storage) should resolve this.
9091
92+### ADFS Auth Provider
93+
94+1. Open the ADFS administration console on your Windows Server and add a new Application Group
95+2. Provide a name for the integration, select Server Application from the Standalone applications section and click Next
96+3. Follow the wizard to get the client-id, client-secret and configure the application credentials
97+4. Configure the proxy with
98+
99+```
100+ --provider=adfs
101+ --client-id=<application ID from step 3>
102+ --client-secret=<value from step 3>
103+```
104+
105+Note: When using the ADFS Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](sessions.md#redis-storage) should resolve this.
106+
91107### Facebook Auth Provider
92108
931091. Create a new FB App from <https://developers.facebook.com/>
pkg/apis/options/providers.go+9−1
@@ -21,6 +21,8 @@ type Provider struct {
2121 KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"`
2222 // AzureConfig holds all configurations for Azure provider.
2323 AzureConfig AzureOptions `json:"azureConfig,omitempty"`
24+ // ADFSConfig holds all configurations for ADFS provider.
25+ ADFSConfig ADFSOptions `json:"ADFSConfig,omitempty"`
2426 // BitbucketConfig holds all configurations for Bitbucket provider.
2527 BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"`
2628 // GitHubConfig holds all configurations for GitHubC provider.
@@ -55,7 +57,7 @@ type Provider struct {
5557 RedeemURL string `json:"redeemURL,omitempty"`
5658 // ProfileURL is the profile access endpoint
5759 ProfileURL string `json:"profileURL,omitempty"`
58− // ProtectedResource is the resource that is protected (Azure AD only)
60+ // ProtectedResource is the resource that is protected (Azure AD and ADFS only)
5961 ProtectedResource string `json:"resource,omitempty"`
6062 // ValidateURL is the access token validation endpoint
6163 ValidateURL string `json:"validateURL,omitempty"`
@@ -84,6 +86,12 @@ type AzureOptions struct {
8486 Tenant string `json:"tenant,omitempty"`
8587}
8688
89+type ADFSOptions struct {
90+ // Skip adding the scope parameter in login request
91+ // Default value is 'false'
92+ SkipScope bool `json:"skipScope,omitempty"`
93+}
94+
8795type BitbucketOptions struct {
8896 // Team sets restrict logins to members of this team
8997 Team string `json:"team,omitempty"`
pkg/validation/options.go+2
@@ -236,6 +236,8 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
236236 switch p := o.GetProvider().(type) {
237237 case *providers.AzureProvider:
238238 p.Configure(o.Providers[0].AzureConfig.Tenant)
239+ case *providers.ADFSProvider:
240+ p.Configure(o.Providers[0].ADFSConfig.SkipScope)
239241 case *providers.GitHubProvider:
240242 p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team)
241243 p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token)
providers/adfs.gonew file+100
@@ -0,0 +1,100 @@
1+package providers
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "net/url"
8+ "strings"
9+
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11+)
12+
13+// ADFSProvider represents an ADFS based Identity Provider
14+type ADFSProvider struct {
15+ *OIDCProvider
16+ SkipScope bool
17+}
18+
19+var _ Provider = (*ADFSProvider)(nil)
20+
21+const (
22+ ADFSProviderName = "ADFS"
23+ ADFSDefaultScope = "openid email profile"
24+ ADFSSkipScope = false
25+)
26+
27+// NewADFSProvider initiates a new ADFSProvider
28+func NewADFSProvider(p *ProviderData) *ADFSProvider {
29+
30+ p.setProviderDefaults(providerDefaults{
31+ name: ADFSProviderName,
32+ scope: ADFSDefaultScope,
33+ })
34+
35+ if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
36+ resource := p.ProtectedResource.String()
37+ if !strings.HasSuffix(resource, "/") {
38+ resource += "/"
39+ }
40+
41+ if p.Scope != "" && !strings.HasPrefix(p.Scope, resource) {
42+ p.Scope = resource + p.Scope
43+ }
44+ }
45+
46+ return &ADFSProvider{
47+ OIDCProvider: &OIDCProvider{
48+ ProviderData: p,
49+ SkipNonce: true,
50+ },
51+ SkipScope: ADFSSkipScope,
52+ }
53+}
54+
55+// Configure defaults the ADFSProvider configuration options
56+func (p *ADFSProvider) Configure(skipScope bool) {
57+ p.SkipScope = skipScope
58+}
59+
60+// GetLoginURL Override to double encode the state parameter. If not query params are lost
61+// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
62+func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
63+ extraParams := url.Values{}
64+ if !p.SkipNonce {
65+ extraParams.Add("nonce", nonce)
66+ }
67+ loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams)
68+ if p.SkipScope {
69+ q := loginURL.Query()
70+ q.Del("scope")
71+ loginURL.RawQuery = q.Encode()
72+ }
73+ return loginURL.String()
74+}
75+
76+// EnrichSession to add email
77+func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
78+ if s.Email != "" {
79+ return nil
80+ }
81+
82+ idToken, err := p.Verifier.Verify(ctx, s.IDToken)
83+ if err != nil {
84+ return err
85+ }
86+
87+ p.EmailClaim = "upn"
88+ c, err := p.getClaims(idToken)
89+
90+ if err != nil {
91+ return fmt.Errorf("couldn't extract claims from id_token (%v)", err)
92+ }
93+ s.Email = c.Email
94+
95+ if s.Email == "" {
96+ err = errors.New("email not set")
97+ }
98+
99+ return err
100+}
providers/adfs_test.gonew file+205
@@ -0,0 +1,205 @@
1+package providers
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "net/http"
7+ "net/http/httptest"
8+ "net/url"
9+ "strings"
10+
11+ "github.com/coreos/go-oidc"
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13+ . "github.com/onsi/ginkgo"
14+ . "github.com/onsi/ginkgo/extensions/table"
15+ . "github.com/onsi/gomega"
16+)
17+
18+type fakeADFSJwks struct{}
19+
20+func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
21+ decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
22+ if err != nil {
23+ return nil, err
24+ }
25+ return decodeString, nil
26+}
27+
28+func testADFSProvider(hostname string) *ADFSProvider {
29+
30+ o := oidc.NewVerifier(
31+ "https://issuer.example.com",
32+ fakeADFSJwks{},
33+ &oidc.Config{ClientID: "https://test.myapp.com"},
34+ )
35+
36+ p := NewADFSProvider(&ProviderData{
37+ ProviderName: "",
38+ LoginURL: &url.URL{},
39+ RedeemURL: &url.URL{},
40+ ProfileURL: &url.URL{},
41+ ValidateURL: &url.URL{},
42+ Scope: "",
43+ Verifier: o,
44+ })
45+
46+ if hostname != "" {
47+ updateURL(p.Data().LoginURL, hostname)
48+ updateURL(p.Data().RedeemURL, hostname)
49+ updateURL(p.Data().ProfileURL, hostname)
50+ updateURL(p.Data().ValidateURL, hostname)
51+ }
52+
53+ return p
54+}
55+
56+func testADFSBackend() *httptest.Server {
57+
58+ authResponse := `
59+ {
60+ "access_token": "my_access_token",
61+ "id_token": "my_id_token",
62+ "refresh_token": "my_refresh_token"
63+ }
64+ `
65+ userInfo := `
66+ {
67+ "email": "samiracho@email.com"
68+ }
69+ `
70+
71+ refreshResponse := `{ "access_token": "new_some_access_token", "refresh_token": "new_some_refresh_token", "expires_in": "32693148245", "id_token": "new_some_id_token" }`
72+
73+ authHeader := "Bearer adfs_access_token"
74+
75+ return httptest.NewServer(http.HandlerFunc(
76+ func(w http.ResponseWriter, r *http.Request) {
77+ switch r.URL.Path {
78+ case "/adfs/oauth2/authorize":
79+ w.WriteHeader(200)
80+ w.Write([]byte(authResponse))
81+ case "/adfs/oauth2/refresh":
82+ w.WriteHeader(200)
83+ w.Write([]byte(refreshResponse))
84+ case "/adfs/oauth2/userinfo":
85+ if r.Header["Authorization"][0] == authHeader {
86+ w.WriteHeader(200)
87+ w.Write([]byte(userInfo))
88+ } else {
89+ w.WriteHeader(401)
90+ }
91+ default:
92+ w.WriteHeader(200)
93+ }
94+ }))
95+}
96+
97+var _ = Describe("ADFS Provider Tests", func() {
98+ var p *ADFSProvider
99+ var b *httptest.Server
100+
101+ BeforeEach(func() {
102+ b = testADFSBackend()
103+
104+ bURL, err := url.Parse(b.URL)
105+ Expect(err).To(BeNil())
106+
107+ p = testADFSProvider(bURL.Host)
108+ })
109+
110+ AfterEach(func() {
111+ b.Close()
112+ })
113+
114+ Context("New Provider Init", func() {
115+ It("uses defaults", func() {
116+ providerData := NewADFSProvider(&ProviderData{}).Data()
117+ Expect(providerData.ProviderName).To(Equal("ADFS"))
118+ Expect(providerData.Scope).To(Equal("openid email profile"))
119+ })
120+ })
121+
122+ Context("with bad token", func() {
123+ It("should trigger an error", func() {
124+ session := &sessions.SessionState{AccessToken: "unexpected_adfs_access_token", IDToken: "malformed_token"}
125+ err := p.EnrichSession(context.Background(), session)
126+ Expect(err).NotTo(BeNil())
127+ })
128+ })
129+
130+ Context("with valid token", func() {
131+ It("should not throw an error", func() {
132+ p.EmailClaim = "email"
133+ rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
134+ idToken, err := p.Verifier.Verify(context.Background(), rawIDToken)
135+ Expect(err).To(BeNil())
136+ session, err := p.buildSessionFromClaims(idToken)
137+ session.IDToken = rawIDToken
138+ Expect(err).To(BeNil())
139+ err = p.EnrichSession(context.Background(), session)
140+ Expect(session.Email).To(Equal("janed@me.com"))
141+ Expect(err).To(BeNil())
142+ })
143+ })
144+
145+ Context("with skipScope enabled", func() {
146+ It("should not include parameter scope", func() {
147+ resource, _ := url.Parse("http://example.com")
148+ p := NewADFSProvider(&ProviderData{
149+ ProtectedResource: resource,
150+ Scope: "",
151+ })
152+ p.SkipScope = true
153+
154+ result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
155+ Expect(result).NotTo(ContainSubstring("scope="))
156+ })
157+ })
158+
159+ Context("With resource parameter", func() {
160+ type scopeTableInput struct {
161+ resource string
162+ scope string
163+ expectedScope string
164+ }
165+
166+ DescribeTable("should return expected results",
167+ func(in scopeTableInput) {
168+ resource, _ := url.Parse(in.resource)
169+ p := NewADFSProvider(&ProviderData{
170+ ProtectedResource: resource,
171+ Scope: in.scope,
172+ })
173+
174+ Expect(p.Data().Scope).To(Equal(in.expectedScope))
175+ result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
176+ Expect(result).To(ContainSubstring("scope=" + url.QueryEscape(in.expectedScope)))
177+ },
178+ Entry("should add slash", scopeTableInput{
179+ resource: "http://resource.com",
180+ scope: "openid",
181+ expectedScope: "http://resource.com/openid",
182+ }),
183+ Entry("shouldn't add extra slash", scopeTableInput{
184+ resource: "http://resource.com/",
185+ scope: "openid",
186+ expectedScope: "http://resource.com/openid",
187+ }),
188+ Entry("should add default scopes with resource", scopeTableInput{
189+ resource: "http://resource.com/",
190+ scope: "",
191+ expectedScope: "http://resource.com/openid email profile",
192+ }),
193+ Entry("should add default scopes", scopeTableInput{
194+ resource: "",
195+ scope: "",
196+ expectedScope: "openid email profile",
197+ }),
198+ Entry("shouldn't add resource if already in scopes", scopeTableInput{
199+ resource: "http://resource.com",
200+ scope: "http://resource.com/openid",
201+ expectedScope: "http://resource.com/openid",
202+ }),
203+ )
204+ })
205+})
providers/providers.go+2
@@ -33,6 +33,8 @@ func New(provider string, p *ProviderData) Provider {
3333 return NewKeycloakProvider(p)
3434 case "azure":
3535 return NewAzureProvider(p)
36+ case "adfs":
37+ return NewADFSProvider(p)
3638 case "gitlab":
3739 return NewGitLabProvider(p)
3840 case "oidc":