Added ADFS Provider
This commit is contained in:
		
							parent
							
								
									381ac91752
								
							
						
					
					
						commit
						a14c0c2121
					
				|  | @ -7,6 +7,7 @@ | |||
| ## Breaking Changes | ||||
| 
 | ||||
| ## Changes since v7.1.3 | ||||
| - [#1238](https://github.com/oauth2-proxy/oauth2-proxy/pull/1238) Added ADFS provider (@samirachoadi) | ||||
| - [#1227](https://github.com/oauth2-proxy/oauth2-proxy/pull/1227) Fix Refresh Session not working for multiple cookies (@rishi1111) | ||||
| - [#1063](https://github.com/oauth2-proxy/oauth2-proxy/pull/1063) Add Redis lock feature to lock persistent sessions (@Bibob7) | ||||
| - [#1108](https://github.com/oauth2-proxy/oauth2-proxy/pull/1108) Add alternative ways to generate cookie secrets to docs (@JoelSpeed) | ||||
|  |  | |||
|  | @ -101,6 +101,16 @@ You must remove these options before starting OAuth2 Proxy with `--alpha-config` | |||
| ## Configuration Reference | ||||
| <!--- THIS FILE IS AUTOGENERATED!!! DO NOT EDIT!!! --> | ||||
| 
 | ||||
| ### ADFSOptions | ||||
| 
 | ||||
| (**Appears on:** [Provider](#provider)) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| | Field | Type | Description | | ||||
| | ----- | ---- | ----------- | | ||||
| | `skipScope` | _bool_ | Skip adding the scope parameter in login request<br/>Default value is 'false' | | ||||
| 
 | ||||
| ### AlphaOptions | ||||
| 
 | ||||
| AlphaOptions contains alpha structured configuration options. | ||||
|  | @ -284,6 +294,7 @@ Provider holds all configuration for a single provider | |||
| | `clientSecretFile` | _string_ | ClientSecretFile is the name of the file<br/>containing the OAuth Client Secret, it will be used if ClientSecret is not set. | | ||||
| | `keycloakConfig` | _[KeycloakOptions](#keycloakoptions)_ | KeycloakConfig holds all configurations for Keycloak provider. | | ||||
| | `azureConfig` | _[AzureOptions](#azureoptions)_ | AzureConfig holds all configurations for Azure provider. | | ||||
| | `ADFSConfig` | _[ADFSOptions](#adfsoptions)_ | ADFSConfig holds all configurations for ADFS provider. | | ||||
| | `bitbucketConfig` | _[BitbucketOptions](#bitbucketoptions)_ | BitbucketConfig holds all configurations for Bitbucket provider. | | ||||
| | `githubConfig` | _[GitHubOptions](#githuboptions)_ | GitHubConfig holds all configurations for GitHubC provider. | | ||||
| | `gitlabConfig` | _[GitLabOptions](#gitlaboptions)_ | GitLabConfig holds all configurations for GitLab provider. | | ||||
|  | @ -297,7 +308,7 @@ Provider holds all configuration for a single provider | |||
| | `loginURL` | _string_ | LoginURL is the authentication endpoint | | ||||
| | `redeemURL` | _string_ | RedeemURL is the token redemption endpoint | | ||||
| | `profileURL` | _string_ | ProfileURL is the profile access endpoint | | ||||
| | `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD only) | | ||||
| | `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD and ADFS only) | | ||||
| | `validateURL` | _string_ | ValidateURL is the access token validation endpoint | | ||||
| | `scope` | _string_ | Scope is the OAuth scope specification | | ||||
| | `prompt` | _string_ | Prompt is OIDC prompt | | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ Valid providers are : | |||
| 
 | ||||
| - [Google](#google-auth-provider) _default_ | ||||
| - [Azure](#azure-auth-provider) | ||||
| - [ADFS](#adfs-auth-provider) | ||||
| - [Facebook](#facebook-auth-provider) | ||||
| - [GitHub](#github-auth-provider) | ||||
| - [Keycloak](#keycloak-auth-provider) | ||||
|  | @ -88,6 +89,21 @@ Note: The user is checked against the group members list on initial authenticati | |||
| 
 | ||||
| Note: 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. | ||||
| 
 | ||||
| ### ADFS Auth Provider | ||||
| 
 | ||||
| 1. Open the ADFS administration console on your Windows Server and add a new Application Group | ||||
| 2. Provide a name for the integration, select Server Application from the Standalone applications section and click Next | ||||
| 3. Follow the wizard to get the client-id, client-secret and configure the application credentials | ||||
| 4. Configure the proxy with | ||||
| 
 | ||||
| ``` | ||||
|    --provider=adfs | ||||
|    --client-id=<application ID from step 3> | ||||
|    --client-secret=<value from step 3> | ||||
| ``` | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| ### Facebook Auth Provider | ||||
| 
 | ||||
| 1.  Create a new FB App from <https://developers.facebook.com/> | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ type Provider struct { | |||
| 	KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"` | ||||
| 	// AzureConfig holds all configurations for Azure provider.
 | ||||
| 	AzureConfig AzureOptions `json:"azureConfig,omitempty"` | ||||
| 	// ADFSConfig holds all configurations for ADFS provider.
 | ||||
| 	ADFSConfig ADFSOptions `json:"ADFSConfig,omitempty"` | ||||
| 	// BitbucketConfig holds all configurations for Bitbucket provider.
 | ||||
| 	BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"` | ||||
| 	// GitHubConfig holds all configurations for GitHubC provider.
 | ||||
|  | @ -55,7 +57,7 @@ type Provider struct { | |||
| 	RedeemURL string `json:"redeemURL,omitempty"` | ||||
| 	// ProfileURL is the profile access endpoint
 | ||||
| 	ProfileURL string `json:"profileURL,omitempty"` | ||||
| 	// ProtectedResource is the resource that is protected (Azure AD only)
 | ||||
| 	// ProtectedResource is the resource that is protected (Azure AD and ADFS only)
 | ||||
| 	ProtectedResource string `json:"resource,omitempty"` | ||||
| 	// ValidateURL is the access token validation endpoint
 | ||||
| 	ValidateURL string `json:"validateURL,omitempty"` | ||||
|  | @ -84,6 +86,12 @@ type AzureOptions struct { | |||
| 	Tenant string `json:"tenant,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type ADFSOptions struct { | ||||
| 	// Skip adding the scope parameter in login request
 | ||||
| 	// Default value is 'false'
 | ||||
| 	SkipScope bool `json:"skipScope,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type BitbucketOptions struct { | ||||
| 	// Team sets restrict logins to members of this team
 | ||||
| 	Team string `json:"team,omitempty"` | ||||
|  |  | |||
|  | @ -236,6 +236,8 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | |||
| 	switch p := o.GetProvider().(type) { | ||||
| 	case *providers.AzureProvider: | ||||
| 		p.Configure(o.Providers[0].AzureConfig.Tenant) | ||||
| 	case *providers.ADFSProvider: | ||||
| 		p.Configure(o.Providers[0].ADFSConfig.SkipScope) | ||||
| 	case *providers.GitHubProvider: | ||||
| 		p.SetOrgTeam(o.Providers[0].GitHubConfig.Org, o.Providers[0].GitHubConfig.Team) | ||||
| 		p.SetRepo(o.Providers[0].GitHubConfig.Repo, o.Providers[0].GitHubConfig.Token) | ||||
|  |  | |||
|  | @ -0,0 +1,100 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||
| ) | ||||
| 
 | ||||
| // ADFSProvider represents an ADFS based Identity Provider
 | ||||
| type ADFSProvider struct { | ||||
| 	*OIDCProvider | ||||
| 	SkipScope bool | ||||
| } | ||||
| 
 | ||||
| var _ Provider = (*ADFSProvider)(nil) | ||||
| 
 | ||||
| const ( | ||||
| 	ADFSProviderName = "ADFS" | ||||
| 	ADFSDefaultScope = "openid email profile" | ||||
| 	ADFSSkipScope    = false | ||||
| ) | ||||
| 
 | ||||
| // NewADFSProvider initiates a new ADFSProvider
 | ||||
| func NewADFSProvider(p *ProviderData) *ADFSProvider { | ||||
| 
 | ||||
| 	p.setProviderDefaults(providerDefaults{ | ||||
| 		name:  ADFSProviderName, | ||||
| 		scope: ADFSDefaultScope, | ||||
| 	}) | ||||
| 
 | ||||
| 	if p.ProtectedResource != nil && p.ProtectedResource.String() != "" { | ||||
| 		resource := p.ProtectedResource.String() | ||||
| 		if !strings.HasSuffix(resource, "/") { | ||||
| 			resource += "/" | ||||
| 		} | ||||
| 
 | ||||
| 		if p.Scope != "" && !strings.HasPrefix(p.Scope, resource) { | ||||
| 			p.Scope = resource + p.Scope | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return &ADFSProvider{ | ||||
| 		OIDCProvider: &OIDCProvider{ | ||||
| 			ProviderData: p, | ||||
| 			SkipNonce:    true, | ||||
| 		}, | ||||
| 		SkipScope: ADFSSkipScope, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Configure defaults the ADFSProvider configuration options
 | ||||
| func (p *ADFSProvider) Configure(skipScope bool) { | ||||
| 	p.SkipScope = skipScope | ||||
| } | ||||
| 
 | ||||
| // GetLoginURL Override to double encode the state parameter. If not query params are lost
 | ||||
| // More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
 | ||||
| func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string { | ||||
| 	extraParams := url.Values{} | ||||
| 	if !p.SkipNonce { | ||||
| 		extraParams.Add("nonce", nonce) | ||||
| 	} | ||||
| 	loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams) | ||||
| 	if p.SkipScope { | ||||
| 		q := loginURL.Query() | ||||
| 		q.Del("scope") | ||||
| 		loginURL.RawQuery = q.Encode() | ||||
| 	} | ||||
| 	return loginURL.String() | ||||
| } | ||||
| 
 | ||||
| // EnrichSession to add email
 | ||||
| func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { | ||||
| 	if s.Email != "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	idToken, err := p.Verifier.Verify(ctx, s.IDToken) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	p.EmailClaim = "upn" | ||||
| 	c, err := p.getClaims(idToken) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("couldn't extract claims from id_token (%v)", err) | ||||
| 	} | ||||
| 	s.Email = c.Email | ||||
| 
 | ||||
| 	if s.Email == "" { | ||||
| 		err = errors.New("email not set") | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
|  | @ -0,0 +1,205 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/coreos/go-oidc" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||
| 	. "github.com/onsi/ginkgo" | ||||
| 	. "github.com/onsi/ginkgo/extensions/table" | ||||
| 	. "github.com/onsi/gomega" | ||||
| ) | ||||
| 
 | ||||
| type fakeADFSJwks struct{} | ||||
| 
 | ||||
| func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) { | ||||
| 	decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1]) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return decodeString, nil | ||||
| } | ||||
| 
 | ||||
| func testADFSProvider(hostname string) *ADFSProvider { | ||||
| 
 | ||||
| 	o := oidc.NewVerifier( | ||||
| 		"https://issuer.example.com", | ||||
| 		fakeADFSJwks{}, | ||||
| 		&oidc.Config{ClientID: "https://test.myapp.com"}, | ||||
| 	) | ||||
| 
 | ||||
| 	p := NewADFSProvider(&ProviderData{ | ||||
| 		ProviderName: "", | ||||
| 		LoginURL:     &url.URL{}, | ||||
| 		RedeemURL:    &url.URL{}, | ||||
| 		ProfileURL:   &url.URL{}, | ||||
| 		ValidateURL:  &url.URL{}, | ||||
| 		Scope:        "", | ||||
| 		Verifier:     o, | ||||
| 	}) | ||||
| 
 | ||||
| 	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 testADFSBackend() *httptest.Server { | ||||
| 
 | ||||
| 	authResponse := ` | ||||
| 		{ | ||||
| 			"access_token": "my_access_token", | ||||
| 			"id_token": "my_id_token", | ||||
| 			"refresh_token": "my_refresh_token"  | ||||
| 		 } | ||||
| 	` | ||||
| 	userInfo := ` | ||||
| 		{ | ||||
| 			"email": "samiracho@email.com" | ||||
| 		} | ||||
| 	` | ||||
| 
 | ||||
| 	refreshResponse := `{ "access_token": "new_some_access_token", "refresh_token": "new_some_refresh_token", "expires_in": "32693148245", "id_token": "new_some_id_token" }` | ||||
| 
 | ||||
| 	authHeader := "Bearer adfs_access_token" | ||||
| 
 | ||||
| 	return httptest.NewServer(http.HandlerFunc( | ||||
| 		func(w http.ResponseWriter, r *http.Request) { | ||||
| 			switch r.URL.Path { | ||||
| 			case "/adfs/oauth2/authorize": | ||||
| 				w.WriteHeader(200) | ||||
| 				w.Write([]byte(authResponse)) | ||||
| 			case "/adfs/oauth2/refresh": | ||||
| 				w.WriteHeader(200) | ||||
| 				w.Write([]byte(refreshResponse)) | ||||
| 			case "/adfs/oauth2/userinfo": | ||||
| 				if r.Header["Authorization"][0] == authHeader { | ||||
| 					w.WriteHeader(200) | ||||
| 					w.Write([]byte(userInfo)) | ||||
| 				} else { | ||||
| 					w.WriteHeader(401) | ||||
| 				} | ||||
| 			default: | ||||
| 				w.WriteHeader(200) | ||||
| 			} | ||||
| 		})) | ||||
| } | ||||
| 
 | ||||
| var _ = Describe("ADFS Provider Tests", func() { | ||||
| 	var p *ADFSProvider | ||||
| 	var b *httptest.Server | ||||
| 
 | ||||
| 	BeforeEach(func() { | ||||
| 		b = testADFSBackend() | ||||
| 
 | ||||
| 		bURL, err := url.Parse(b.URL) | ||||
| 		Expect(err).To(BeNil()) | ||||
| 
 | ||||
| 		p = testADFSProvider(bURL.Host) | ||||
| 	}) | ||||
| 
 | ||||
| 	AfterEach(func() { | ||||
| 		b.Close() | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("New Provider Init", func() { | ||||
| 		It("uses defaults", func() { | ||||
| 			providerData := NewADFSProvider(&ProviderData{}).Data() | ||||
| 			Expect(providerData.ProviderName).To(Equal("ADFS")) | ||||
| 			Expect(providerData.Scope).To(Equal("openid email profile")) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("with bad token", func() { | ||||
| 		It("should trigger an error", func() { | ||||
| 			session := &sessions.SessionState{AccessToken: "unexpected_adfs_access_token", IDToken: "malformed_token"} | ||||
| 			err := p.EnrichSession(context.Background(), session) | ||||
| 			Expect(err).NotTo(BeNil()) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("with valid token", func() { | ||||
| 		It("should not throw an error", func() { | ||||
| 			p.EmailClaim = "email" | ||||
| 			rawIDToken, _ := newSignedTestIDToken(defaultIDToken) | ||||
| 			idToken, err := p.Verifier.Verify(context.Background(), rawIDToken) | ||||
| 			Expect(err).To(BeNil()) | ||||
| 			session, err := p.buildSessionFromClaims(idToken) | ||||
| 			session.IDToken = rawIDToken | ||||
| 			Expect(err).To(BeNil()) | ||||
| 			err = p.EnrichSession(context.Background(), session) | ||||
| 			Expect(session.Email).To(Equal("janed@me.com")) | ||||
| 			Expect(err).To(BeNil()) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("with skipScope enabled", func() { | ||||
| 		It("should not include parameter scope", func() { | ||||
| 			resource, _ := url.Parse("http://example.com") | ||||
| 			p := NewADFSProvider(&ProviderData{ | ||||
| 				ProtectedResource: resource, | ||||
| 				Scope:             "", | ||||
| 			}) | ||||
| 			p.SkipScope = true | ||||
| 
 | ||||
| 			result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "") | ||||
| 			Expect(result).NotTo(ContainSubstring("scope=")) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("With resource parameter", func() { | ||||
| 		type scopeTableInput struct { | ||||
| 			resource      string | ||||
| 			scope         string | ||||
| 			expectedScope string | ||||
| 		} | ||||
| 
 | ||||
| 		DescribeTable("should return expected results", | ||||
| 			func(in scopeTableInput) { | ||||
| 				resource, _ := url.Parse(in.resource) | ||||
| 				p := NewADFSProvider(&ProviderData{ | ||||
| 					ProtectedResource: resource, | ||||
| 					Scope:             in.scope, | ||||
| 				}) | ||||
| 
 | ||||
| 				Expect(p.Data().Scope).To(Equal(in.expectedScope)) | ||||
| 				result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "") | ||||
| 				Expect(result).To(ContainSubstring("scope=" + url.QueryEscape(in.expectedScope))) | ||||
| 			}, | ||||
| 			Entry("should add slash", scopeTableInput{ | ||||
| 				resource:      "http://resource.com", | ||||
| 				scope:         "openid", | ||||
| 				expectedScope: "http://resource.com/openid", | ||||
| 			}), | ||||
| 			Entry("shouldn't add extra slash", scopeTableInput{ | ||||
| 				resource:      "http://resource.com/", | ||||
| 				scope:         "openid", | ||||
| 				expectedScope: "http://resource.com/openid", | ||||
| 			}), | ||||
| 			Entry("should add default scopes with resource", scopeTableInput{ | ||||
| 				resource:      "http://resource.com/", | ||||
| 				scope:         "", | ||||
| 				expectedScope: "http://resource.com/openid email profile", | ||||
| 			}), | ||||
| 			Entry("should add default scopes", scopeTableInput{ | ||||
| 				resource:      "", | ||||
| 				scope:         "", | ||||
| 				expectedScope: "openid email profile", | ||||
| 			}), | ||||
| 			Entry("shouldn't add resource if already in scopes", scopeTableInput{ | ||||
| 				resource:      "http://resource.com", | ||||
| 				scope:         "http://resource.com/openid", | ||||
| 				expectedScope: "http://resource.com/openid", | ||||
| 			}), | ||||
| 		) | ||||
| 	}) | ||||
| }) | ||||
|  | @ -33,6 +33,8 @@ func New(provider string, p *ProviderData) Provider { | |||
| 		return NewKeycloakProvider(p) | ||||
| 	case "azure": | ||||
| 		return NewAzureProvider(p) | ||||
| 	case "adfs": | ||||
| 		return NewADFSProvider(p) | ||||
| 	case "gitlab": | ||||
| 		return NewGitLabProvider(p) | ||||
| 	case "oidc": | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue