Add Azure Provider
This commit is contained in:
		
							parent
							
								
									d5a332c3f2
								
							
						
					
					
						commit
						10f47e325b
					
				
							
								
								
									
										19
									
								
								README.md
								
								
								
								
							
							
						
						
									
										19
									
								
								README.md
								
								
								
								
							|  | @ -29,6 +29,8 @@ You will need to register an OAuth application with a Provider (Google, Github o | |||
| Valid providers are : | ||||
| 
 | ||||
| * [Google](#google-auth-provider) *default* | ||||
| 
 | ||||
| * [Azure](#azure-auth-provider) | ||||
| * [GitHub](#github-auth-provider) | ||||
| * [LinkedIn](#linkedin-auth-provider) | ||||
| * [MyUSA](#myusa-auth-provider) | ||||
|  | @ -76,6 +78,15 @@ and the user will be checked against all the provided groups. | |||
| 
 | ||||
| Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ). | ||||
| 
 | ||||
| ### Azure Auth Provider | ||||
| 
 | ||||
| 1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant. | ||||
| 2. On the App properties page provide the correct Sign-On URL ie `https//internal.yourcompany.com/oauth2/callback` | ||||
| 3. If applicable take note of your `TenantID` and provide it via the `--azure-tenant=<YOUR TENANT ID>` commandline option. Default the `common` tenant is used. | ||||
| 
 | ||||
| The Azure AD auth provider uses `openid` as it default scope. It uses `https://graph.windows.net` as a default protected resource. It call to `https://graph.windows.net/me` to get the email address of the user that logs in. | ||||
| 
 | ||||
| 
 | ||||
| ### GitHub Auth Provider | ||||
| 
 | ||||
| 1. Create a new project: https://github.com/settings/developers | ||||
|  | @ -102,6 +113,12 @@ For LinkedIn, the registration steps are: | |||
| 
 | ||||
| The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa)) | ||||
| 
 | ||||
| ### Microsoft Azure AD Provider | ||||
| 
 | ||||
| For adding an application to the Microsoft Azure AD follow [these steps to add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/). | ||||
| 
 | ||||
| Take note of your `TenantId` if applicable for your situation. The `TenantId` can be used to override the default `common` authorization server with a tenant specific server. | ||||
| 
 | ||||
| ## 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 addresse use `--email-domain=*`. | ||||
|  | @ -120,6 +137,7 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i | |||
| Usage of oauth2_proxy: | ||||
|   -approval-prompt="force": Oauth approval_prompt | ||||
|   -authenticated-emails-file="": authenticate against emails via file (one per line) | ||||
|   -azure-tenant="common": go to a tenant-specific or common (tenant-independent) endpoint. | ||||
|   -basic-auth-password="": the password to set when passing the HTTP Basic Auth header | ||||
|   -client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com" | ||||
|   -client-secret="": the OAuth Client Secret | ||||
|  | @ -151,6 +169,7 @@ Usage of oauth2_proxy: | |||
|   -proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in) | ||||
|   -redeem-url="": Token redemption endpoint | ||||
|   -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" | ||||
|   -resource="": the resource that is being protected. ie: "https://graph.windows.net". Currently only used in the Azure provider. | ||||
|   -request-logging=true: Log requests to stdout | ||||
|   -scope="": Oauth scope specification | ||||
|   -signature-key="": GAP-Signature request signature key (algorithm:secretkey) | ||||
|  |  | |||
							
								
								
									
										2
									
								
								main.go
								
								
								
								
							
							
						
						
									
										2
									
								
								main.go
								
								
								
								
							|  | @ -38,6 +38,7 @@ func main() { | |||
| 	flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)") | ||||
| 
 | ||||
| 	flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") | ||||
| 	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") | ||||
| 	flagSet.String("github-org", "", "restrict logins to members of this organisation") | ||||
| 	flagSet.String("github-team", "", "restrict logins to members of this team") | ||||
| 	flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") | ||||
|  | @ -65,6 +66,7 @@ func main() { | |||
| 	flagSet.String("login-url", "", "Authentication endpoint") | ||||
| 	flagSet.String("redeem-url", "", "Token redemption endpoint") | ||||
| 	flagSet.String("profile-url", "", "Profile access endpoint") | ||||
| 	flagSet.String("resource", "", "The resource that is protected (Azure AD only)") | ||||
| 	flagSet.String("validate-url", "", "Access token validation endpoint") | ||||
| 	flagSet.String("scope", "", "OAuth scope specification") | ||||
| 	flagSet.String("approval-prompt", "force", "OAuth approval_prompt") | ||||
|  |  | |||
							
								
								
									
										19
									
								
								options.go
								
								
								
								
							
							
						
						
									
										19
									
								
								options.go
								
								
								
								
							|  | @ -25,6 +25,7 @@ type Options struct { | |||
| 	TLSKeyFile   string `flag:"tls-key" cfg:"tls_key_file"` | ||||
| 
 | ||||
| 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` | ||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | ||||
| 	EmailDomains             []string `flag:"email-domain" cfg:"email_domains"` | ||||
| 	GitHubOrg                string   `flag:"github-org" cfg:"github_org"` | ||||
| 	GitHubTeam               string   `flag:"github-team" cfg:"github_team"` | ||||
|  | @ -52,13 +53,14 @@ type Options struct { | |||
| 
 | ||||
| 	// These options allow for other providers besides Google, with
 | ||||
| 	// potential overrides.
 | ||||
| 	Provider       string `flag:"provider" cfg:"provider"` | ||||
| 	LoginURL       string `flag:"login-url" cfg:"login_url"` | ||||
| 	RedeemURL      string `flag:"redeem-url" cfg:"redeem_url"` | ||||
| 	ProfileURL     string `flag:"profile-url" cfg:"profile_url"` | ||||
| 	ValidateURL    string `flag:"validate-url" cfg:"validate_url"` | ||||
| 	Scope          string `flag:"scope" cfg:"scope"` | ||||
| 	ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` | ||||
| 	Provider          string `flag:"provider" cfg:"provider"` | ||||
| 	LoginURL          string `flag:"login-url" cfg:"login_url"` | ||||
| 	RedeemURL         string `flag:"redeem-url" cfg:"redeem_url"` | ||||
| 	ProfileURL        string `flag:"profile-url" cfg:"profile_url"` | ||||
| 	ProtectedResource string `flag:"resource" cfg:"resource"` | ||||
| 	ValidateURL       string `flag:"validate-url" cfg:"validate_url"` | ||||
| 	Scope             string `flag:"scope" cfg:"scope"` | ||||
| 	ApprovalPrompt    string `flag:"approval-prompt" cfg:"approval_prompt"` | ||||
| 
 | ||||
| 	RequestLogging bool `flag:"request-logging" cfg:"request_logging"` | ||||
| 
 | ||||
|  | @ -205,9 +207,12 @@ func parseProviderInfo(o *Options, msgs []string) []string { | |||
| 	p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs) | ||||
| 	p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs) | ||||
| 	p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs) | ||||
| 	p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs) | ||||
| 
 | ||||
| 	o.provider = providers.New(o.Provider, p) | ||||
| 	switch p := o.provider.(type) { | ||||
| 	case *providers.AzureProvider: | ||||
| 		p.Configure(o.AzureTenant) | ||||
| 	case *providers.GitHubProvider: | ||||
| 		p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam) | ||||
| 	case *providers.GoogleProvider: | ||||
|  |  | |||
|  | @ -0,0 +1,86 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/bitly/oauth2_proxy/api" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| ) | ||||
| 
 | ||||
| type AzureProvider struct { | ||||
| 	*ProviderData | ||||
| 	Tenant string | ||||
| } | ||||
| 
 | ||||
| func NewAzureProvider(p *ProviderData) *AzureProvider { | ||||
| 	p.ProviderName = "Azure" | ||||
| 
 | ||||
| 	if p.ProfileURL == nil || p.ProfileURL.String() == "" { | ||||
| 		p.ProfileURL = &url.URL{ | ||||
| 			Scheme:   "https", | ||||
| 			Host:     "graph.windows.net", | ||||
| 			Path:     "/me", | ||||
| 			RawQuery: "api-version=1.6", | ||||
| 		} | ||||
| 	} | ||||
| 	if p.ProtectedResource == nil || p.ProtectedResource.String() == "" { | ||||
| 		p.ProtectedResource = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "graph.windows.net", | ||||
| 		} | ||||
| 	} | ||||
| 	if p.Scope == "" { | ||||
| 		p.Scope = "openid" | ||||
| 	} | ||||
| 
 | ||||
| 	return &AzureProvider{ProviderData: p} | ||||
| } | ||||
| 
 | ||||
| func (p *AzureProvider) Configure(tenant string) { | ||||
| 	p.Tenant = tenant | ||||
| 	if tenant == "" { | ||||
| 		p.Tenant = "common" | ||||
| 	} | ||||
| 
 | ||||
| 	if p.LoginURL == nil || p.LoginURL.String() == "" { | ||||
| 		p.LoginURL = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "login.microsoftonline.com", | ||||
| 			Path:   "/" + p.Tenant + "/oauth2/authorize"} | ||||
| 	} | ||||
| 	if p.RedeemURL == nil || p.RedeemURL.String() == "" { | ||||
| 		p.RedeemURL = &url.URL{ | ||||
| 			Scheme: "https", | ||||
| 			Host:   "login.microsoftonline.com", | ||||
| 			Path:   "/" + p.Tenant + "/oauth2/token", | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getAzureHeader(access_token string) http.Header { | ||||
| 	header := make(http.Header) | ||||
| 	header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token)) | ||||
| 	return header | ||||
| } | ||||
| 
 | ||||
| func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) { | ||||
| 	if s.AccessToken == "" { | ||||
| 		return "", errors.New("missing access token") | ||||
| 	} | ||||
| 	req, err := http.NewRequest("GET", p.ProfileURL.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	req.Header = getAzureHeader(s.AccessToken) | ||||
| 
 | ||||
| 	json, err := api.Request(req) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Printf("failed making request %s", err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return json.Get("mail").String() | ||||
| } | ||||
|  | @ -0,0 +1,135 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/bmizerany/assert" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func testAzureProvider(hostname string) *AzureProvider { | ||||
| 	p := NewAzureProvider( | ||||
| 		&ProviderData{ | ||||
| 			ProviderName:      "", | ||||
| 			LoginURL:          &url.URL{}, | ||||
| 			RedeemURL:         &url.URL{}, | ||||
| 			ProfileURL:        &url.URL{}, | ||||
| 			ValidateURL:       &url.URL{}, | ||||
| 			ProtectedResource: &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) | ||||
| 		updateURL(p.Data().ProtectedResource, hostname) | ||||
| 	} | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
| func TestAzureProviderDefaults(t *testing.T) { | ||||
| 	p := testAzureProvider("") | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	p.Configure("") | ||||
| 	assert.Equal(t, "Azure", p.Data().ProviderName) | ||||
| 	assert.Equal(t, "common", p.Tenant) | ||||
| 	assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/authorize", | ||||
| 		p.Data().LoginURL.String()) | ||||
| 	assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/token", | ||||
| 		p.Data().RedeemURL.String()) | ||||
| 	assert.Equal(t, "https://graph.windows.net/me?api-version=1.6", | ||||
| 		p.Data().ProfileURL.String()) | ||||
| 	assert.Equal(t, "https://graph.windows.net", | ||||
| 		p.Data().ProtectedResource.String()) | ||||
| 	assert.Equal(t, "", | ||||
| 		p.Data().ValidateURL.String()) | ||||
| 	assert.Equal(t, "openid", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
| func TestAzureProviderOverrides(t *testing.T) { | ||||
| 	p := NewAzureProvider( | ||||
| 		&ProviderData{ | ||||
| 			LoginURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/auth"}, | ||||
| 			RedeemURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/token"}, | ||||
| 			ProfileURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/profile"}, | ||||
| 			ValidateURL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com", | ||||
| 				Path:   "/oauth/tokeninfo"}, | ||||
| 			ProtectedResource: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   "example.com"}, | ||||
| 			Scope: "profile"}) | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	assert.Equal(t, "Azure", p.Data().ProviderName) | ||||
| 	assert.Equal(t, "https://example.com/oauth/auth", | ||||
| 		p.Data().LoginURL.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/token", | ||||
| 		p.Data().RedeemURL.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/profile", | ||||
| 		p.Data().ProfileURL.String()) | ||||
| 	assert.Equal(t, "https://example.com/oauth/tokeninfo", | ||||
| 		p.Data().ValidateURL.String()) | ||||
| 	assert.Equal(t, "https://example.com", | ||||
| 		p.Data().ProtectedResource.String()) | ||||
| 	assert.Equal(t, "profile", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
| func TestAzureSetTenant(t *testing.T) { | ||||
| 	p := testAzureProvider("") | ||||
| 	p.Configure("example") | ||||
| 	assert.Equal(t, "Azure", p.Data().ProviderName) | ||||
| 	assert.Equal(t, "example", p.Tenant) | ||||
| 	assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize", | ||||
| 		p.Data().LoginURL.String()) | ||||
| 	assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token", | ||||
| 		p.Data().RedeemURL.String()) | ||||
| 	assert.Equal(t, "https://graph.windows.net/me?api-version=1.6", | ||||
| 		p.Data().ProfileURL.String()) | ||||
| 	assert.Equal(t, "https://graph.windows.net", | ||||
| 		p.Data().ProtectedResource.String()) | ||||
| 	assert.Equal(t, "", | ||||
| 		p.Data().ValidateURL.String()) | ||||
| 	assert.Equal(t, "openid", p.Data().Scope) | ||||
| } | ||||
| 
 | ||||
| func testAzureBackend(payload string) *httptest.Server { | ||||
| 	path := "/me" | ||||
| 	query := "api-version=1.6" | ||||
| 
 | ||||
| 	return httptest.NewServer(http.HandlerFunc( | ||||
| 		func(w http.ResponseWriter, r *http.Request) { | ||||
| 			url := r.URL | ||||
| 			if url.Path != path || url.RawQuery != query { | ||||
| 				w.WriteHeader(404) | ||||
| 			} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" { | ||||
| 				w.WriteHeader(403) | ||||
| 			} else { | ||||
| 				w.WriteHeader(200) | ||||
| 				w.Write([]byte(payload)) | ||||
| 			} | ||||
| 		})) | ||||
| } | ||||
| 
 | ||||
| func TestAzureProviderGetEmailAddress(t *testing.T) { | ||||
| 	b := testAzureBackend(`{ "mail": "user@windows.net" }`) | ||||
| 	defer b.Close() | ||||
| 
 | ||||
| 	b_url, _ := url.Parse(b.URL) | ||||
| 	p := testAzureProvider(b_url.Host) | ||||
| 
 | ||||
| 	session := &SessionState{AccessToken: "imaginary_access_token"} | ||||
| 	email, err := p.GetEmailAddress(session) | ||||
| 	assert.Equal(t, nil, err) | ||||
| 	assert.Equal(t, "user@windows.net", email) | ||||
| } | ||||
|  | @ -5,15 +5,16 @@ import ( | |||
| ) | ||||
| 
 | ||||
| type ProviderData struct { | ||||
| 	ProviderName   string | ||||
| 	ClientID       string | ||||
| 	ClientSecret   string | ||||
| 	LoginURL       *url.URL | ||||
| 	RedeemURL      *url.URL | ||||
| 	ProfileURL     *url.URL | ||||
| 	ValidateURL    *url.URL | ||||
| 	Scope          string | ||||
| 	ApprovalPrompt string | ||||
| 	ProviderName      string | ||||
| 	ClientID          string | ||||
| 	ClientSecret      string | ||||
| 	LoginURL          *url.URL | ||||
| 	RedeemURL         *url.URL | ||||
| 	ProfileURL        *url.URL | ||||
| 	ProtectedResource *url.URL | ||||
| 	ValidateURL       *url.URL | ||||
| 	Scope             string | ||||
| 	ApprovalPrompt    string | ||||
| } | ||||
| 
 | ||||
| func (p *ProviderData) Data() *ProviderData { return p } | ||||
|  |  | |||
|  | @ -25,6 +25,10 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er | |||
| 	params.Add("client_secret", p.ClientSecret) | ||||
| 	params.Add("code", code) | ||||
| 	params.Add("grant_type", "authorization_code") | ||||
| 	if p.ProtectedResource != nil && p.ProtectedResource.String() != "" { | ||||
| 		params.Add("resource", p.ProtectedResource.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	var req *http.Request | ||||
| 	req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode())) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -24,6 +24,8 @@ func New(provider string, p *ProviderData) Provider { | |||
| 		return NewLinkedInProvider(p) | ||||
| 	case "github": | ||||
| 		return NewGitHubProvider(p) | ||||
| 	case "azure": | ||||
| 		return NewAzureProvider(p) | ||||
| 	default: | ||||
| 		return NewGoogleProvider(p) | ||||
| 	} | ||||
|  |  | |||
|  | @ -41,9 +41,8 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) { | |||
| 		for { | ||||
| 			select { | ||||
| 			case _ = <-done: | ||||
| 				log.Printf("Shutting down watcher for: %s", | ||||
| 					filename) | ||||
| 				return | ||||
| 				log.Printf("Shutting down watcher for: %s", filename) | ||||
| 				break | ||||
| 			case event := <-watcher.Events: | ||||
| 				// On Arch Linux, it appears Chmod events precede Remove events,
 | ||||
| 				// which causes a race between action() and the coming Remove event.
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue