git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add Azure Provider
commit10f47e325b782a60b8689653fa45360dee7fbf34 authorEelco Cramer <eelco@servicelab.org> dateMon 9 Nov 2015, 09:28 (11 years ago) parentd5a332c
9 files changed+272−19
README.md+19 main.go+2 options.go+12−7 providers/azure.go+86 providers/azure_test.go+135 providers/provider_data.go+10−9 providers/provider_default.go+4 providers/providers.go+2 watcher.go+2−3
README.md+19
@@ -29,6 +29,8 @@ You will need to register an OAuth application with a Provider (Google, Github o
2929Valid providers are :
3030
3131* [Google](#google-auth-provider) *default*
32+
33+* [Azure](#azure-auth-provider)
3234* [GitHub](#github-auth-provider)
3335* [LinkedIn](#linkedin-auth-provider)
3436* [MyUSA](#myusa-auth-provider)
@@ -76,6 +78,15 @@ and the user will be checked against all the provided groups.
7678
7779Note: The user is checked against the group members list on initial authentication and every time the token is refreshed ( about once an hour ).
7880
81+### Azure Auth Provider
82+
83+1. [Add an application](https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/) to your Azure Active Directory tenant.
84+2. On the App properties page provide the correct Sign-On URL ie `https//internal.yourcompany.com/oauth2/callback`
85+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.
86+
87+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.
88+
89+
7990### GitHub Auth Provider
8091
81921. Create a new project: https://github.com/settings/developers
@@ -102,6 +113,12 @@ For LinkedIn, the registration steps are:
102113
103114The [MyUSA](https://alpha.my.usa.gov) authentication service ([GitHub](https://github.com/18F/myusa))
104115
116+### Microsoft Azure AD Provider
117+
118+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/).
119+
120+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.
121+
105122## Email Authentication
106123
107124To 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
120137Usage of oauth2_proxy:
121138 -approval-prompt="force": Oauth approval_prompt
122139 -authenticated-emails-file="": authenticate against emails via file (one per line)
140+ -azure-tenant="common": go to a tenant-specific or common (tenant-independent) endpoint.
123141 -basic-auth-password="": the password to set when passing the HTTP Basic Auth header
124142 -client-id="": the OAuth Client ID: ie: "123456.apps.googleusercontent.com"
125143 -client-secret="": the OAuth Client Secret
@@ -151,6 +169,7 @@ Usage of oauth2_proxy:
151169 -proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)
152170 -redeem-url="": Token redemption endpoint
153171 -redirect-url="": the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
172+ -resource="": the resource that is being protected. ie: "https://graph.windows.net". Currently only used in the Azure provider.
154173 -request-logging=true: Log requests to stdout
155174 -scope="": Oauth scope specification
156175 -signature-key="": GAP-Signature request signature key (algorithm:secretkey)
main.go+2
@@ -38,6 +38,7 @@ func main() {
3838 flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
3939
4040 flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
41+ flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
4142 flagSet.String("github-org", "", "restrict logins to members of this organisation")
4243 flagSet.String("github-team", "", "restrict logins to members of this team")
4344 flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).")
@@ -65,6 +66,7 @@ func main() {
6566 flagSet.String("login-url", "", "Authentication endpoint")
6667 flagSet.String("redeem-url", "", "Token redemption endpoint")
6768 flagSet.String("profile-url", "", "Profile access endpoint")
69+ flagSet.String("resource", "", "The resource that is protected (Azure AD only)")
6870 flagSet.String("validate-url", "", "Access token validation endpoint")
6971 flagSet.String("scope", "", "OAuth scope specification")
7072 flagSet.String("approval-prompt", "force", "OAuth approval_prompt")
options.go+12−7
@@ -25,6 +25,7 @@ type Options struct {
2525 TLSKeyFile string `flag:"tls-key" cfg:"tls_key_file"`
2626
2727 AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
28+ AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
2829 EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
2930 GitHubOrg string `flag:"github-org" cfg:"github_org"`
3031 GitHubTeam string `flag:"github-team" cfg:"github_team"`
@@ -52,13 +53,14 @@ type Options struct {
5253
5354 // These options allow for other providers besides Google, with
5455 // potential overrides.
55− Provider string `flag:"provider" cfg:"provider"`
56− LoginURL string `flag:"login-url" cfg:"login_url"`
57− RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
58− ProfileURL string `flag:"profile-url" cfg:"profile_url"`
59− ValidateURL string `flag:"validate-url" cfg:"validate_url"`
60− Scope string `flag:"scope" cfg:"scope"`
61− ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
56+ Provider string `flag:"provider" cfg:"provider"`
57+ LoginURL string `flag:"login-url" cfg:"login_url"`
58+ RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
59+ ProfileURL string `flag:"profile-url" cfg:"profile_url"`
60+ ProtectedResource string `flag:"resource" cfg:"resource"`
61+ ValidateURL string `flag:"validate-url" cfg:"validate_url"`
62+ Scope string `flag:"scope" cfg:"scope"`
63+ ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`
6264
6365 RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
6466
@@ -205,9 +207,12 @@ func parseProviderInfo(o *Options, msgs []string) []string {
205207 p.RedeemURL, msgs = parseURL(o.RedeemURL, "redeem", msgs)
206208 p.ProfileURL, msgs = parseURL(o.ProfileURL, "profile", msgs)
207209 p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
210+ p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
208211
209212 o.provider = providers.New(o.Provider, p)
210213 switch p := o.provider.(type) {
214+ case *providers.AzureProvider:
215+ p.Configure(o.AzureTenant)
211216 case *providers.GitHubProvider:
212217 p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
213218 case *providers.GoogleProvider:
providers/azure.gonew file+86
@@ -0,0 +1,86 @@
1+package providers
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "github.com/bitly/oauth2_proxy/api"
7+ "log"
8+ "net/http"
9+ "net/url"
10+)
11+
12+type AzureProvider struct {
13+ *ProviderData
14+ Tenant string
15+}
16+
17+func NewAzureProvider(p *ProviderData) *AzureProvider {
18+ p.ProviderName = "Azure"
19+
20+ if p.ProfileURL == nil || p.ProfileURL.String() == "" {
21+ p.ProfileURL = &url.URL{
22+ Scheme: "https",
23+ Host: "graph.windows.net",
24+ Path: "/me",
25+ RawQuery: "api-version=1.6",
26+ }
27+ }
28+ if p.ProtectedResource == nil || p.ProtectedResource.String() == "" {
29+ p.ProtectedResource = &url.URL{
30+ Scheme: "https",
31+ Host: "graph.windows.net",
32+ }
33+ }
34+ if p.Scope == "" {
35+ p.Scope = "openid"
36+ }
37+
38+ return &AzureProvider{ProviderData: p}
39+}
40+
41+func (p *AzureProvider) Configure(tenant string) {
42+ p.Tenant = tenant
43+ if tenant == "" {
44+ p.Tenant = "common"
45+ }
46+
47+ if p.LoginURL == nil || p.LoginURL.String() == "" {
48+ p.LoginURL = &url.URL{
49+ Scheme: "https",
50+ Host: "login.microsoftonline.com",
51+ Path: "/" + p.Tenant + "/oauth2/authorize"}
52+ }
53+ if p.RedeemURL == nil || p.RedeemURL.String() == "" {
54+ p.RedeemURL = &url.URL{
55+ Scheme: "https",
56+ Host: "login.microsoftonline.com",
57+ Path: "/" + p.Tenant + "/oauth2/token",
58+ }
59+ }
60+}
61+
62+func getAzureHeader(access_token string) http.Header {
63+ header := make(http.Header)
64+ header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
65+ return header
66+}
67+
68+func (p *AzureProvider) GetEmailAddress(s *SessionState) (string, error) {
69+ if s.AccessToken == "" {
70+ return "", errors.New("missing access token")
71+ }
72+ req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
73+ if err != nil {
74+ return "", err
75+ }
76+ req.Header = getAzureHeader(s.AccessToken)
77+
78+ json, err := api.Request(req)
79+
80+ if err != nil {
81+ log.Printf("failed making request %s", err)
82+ return "", err
83+ }
84+
85+ return json.Get("mail").String()
86+}
providers/azure_test.gonew file+135
@@ -0,0 +1,135 @@
1+package providers
2+
3+import (
4+ "github.com/bmizerany/assert"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+)
10+
11+func testAzureProvider(hostname string) *AzureProvider {
12+ p := NewAzureProvider(
13+ &ProviderData{
14+ ProviderName: "",
15+ LoginURL: &url.URL{},
16+ RedeemURL: &url.URL{},
17+ ProfileURL: &url.URL{},
18+ ValidateURL: &url.URL{},
19+ ProtectedResource: &url.URL{},
20+ Scope: ""})
21+ if hostname != "" {
22+ updateURL(p.Data().LoginURL, hostname)
23+ updateURL(p.Data().RedeemURL, hostname)
24+ updateURL(p.Data().ProfileURL, hostname)
25+ updateURL(p.Data().ValidateURL, hostname)
26+ updateURL(p.Data().ProtectedResource, hostname)
27+ }
28+ return p
29+}
30+
31+func TestAzureProviderDefaults(t *testing.T) {
32+ p := testAzureProvider("")
33+ assert.NotEqual(t, nil, p)
34+ p.Configure("")
35+ assert.Equal(t, "Azure", p.Data().ProviderName)
36+ assert.Equal(t, "common", p.Tenant)
37+ assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/authorize",
38+ p.Data().LoginURL.String())
39+ assert.Equal(t, "https://login.microsoftonline.com/common/oauth2/token",
40+ p.Data().RedeemURL.String())
41+ assert.Equal(t, "https://graph.windows.net/me?api-version=1.6",
42+ p.Data().ProfileURL.String())
43+ assert.Equal(t, "https://graph.windows.net",
44+ p.Data().ProtectedResource.String())
45+ assert.Equal(t, "",
46+ p.Data().ValidateURL.String())
47+ assert.Equal(t, "openid", p.Data().Scope)
48+}
49+
50+func TestAzureProviderOverrides(t *testing.T) {
51+ p := NewAzureProvider(
52+ &ProviderData{
53+ LoginURL: &url.URL{
54+ Scheme: "https",
55+ Host: "example.com",
56+ Path: "/oauth/auth"},
57+ RedeemURL: &url.URL{
58+ Scheme: "https",
59+ Host: "example.com",
60+ Path: "/oauth/token"},
61+ ProfileURL: &url.URL{
62+ Scheme: "https",
63+ Host: "example.com",
64+ Path: "/oauth/profile"},
65+ ValidateURL: &url.URL{
66+ Scheme: "https",
67+ Host: "example.com",
68+ Path: "/oauth/tokeninfo"},
69+ ProtectedResource: &url.URL{
70+ Scheme: "https",
71+ Host: "example.com"},
72+ Scope: "profile"})
73+ assert.NotEqual(t, nil, p)
74+ assert.Equal(t, "Azure", p.Data().ProviderName)
75+ assert.Equal(t, "https://example.com/oauth/auth",
76+ p.Data().LoginURL.String())
77+ assert.Equal(t, "https://example.com/oauth/token",
78+ p.Data().RedeemURL.String())
79+ assert.Equal(t, "https://example.com/oauth/profile",
80+ p.Data().ProfileURL.String())
81+ assert.Equal(t, "https://example.com/oauth/tokeninfo",
82+ p.Data().ValidateURL.String())
83+ assert.Equal(t, "https://example.com",
84+ p.Data().ProtectedResource.String())
85+ assert.Equal(t, "profile", p.Data().Scope)
86+}
87+
88+func TestAzureSetTenant(t *testing.T) {
89+ p := testAzureProvider("")
90+ p.Configure("example")
91+ assert.Equal(t, "Azure", p.Data().ProviderName)
92+ assert.Equal(t, "example", p.Tenant)
93+ assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
94+ p.Data().LoginURL.String())
95+ assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token",
96+ p.Data().RedeemURL.String())
97+ assert.Equal(t, "https://graph.windows.net/me?api-version=1.6",
98+ p.Data().ProfileURL.String())
99+ assert.Equal(t, "https://graph.windows.net",
100+ p.Data().ProtectedResource.String())
101+ assert.Equal(t, "",
102+ p.Data().ValidateURL.String())
103+ assert.Equal(t, "openid", p.Data().Scope)
104+}
105+
106+func testAzureBackend(payload string) *httptest.Server {
107+ path := "/me"
108+ query := "api-version=1.6"
109+
110+ return httptest.NewServer(http.HandlerFunc(
111+ func(w http.ResponseWriter, r *http.Request) {
112+ url := r.URL
113+ if url.Path != path || url.RawQuery != query {
114+ w.WriteHeader(404)
115+ } else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
116+ w.WriteHeader(403)
117+ } else {
118+ w.WriteHeader(200)
119+ w.Write([]byte(payload))
120+ }
121+ }))
122+}
123+
124+func TestAzureProviderGetEmailAddress(t *testing.T) {
125+ b := testAzureBackend(`{ "mail": "user@windows.net" }`)
126+ defer b.Close()
127+
128+ b_url, _ := url.Parse(b.URL)
129+ p := testAzureProvider(b_url.Host)
130+
131+ session := &SessionState{AccessToken: "imaginary_access_token"}
132+ email, err := p.GetEmailAddress(session)
133+ assert.Equal(t, nil, err)
134+ assert.Equal(t, "user@windows.net", email)
135+}
providers/provider_data.go+10−9
@@ -5,15 +5,16 @@ import (
55)
66
77type ProviderData struct {
8− ProviderName string
9− ClientID string
10− ClientSecret string
11− LoginURL *url.URL
12− RedeemURL *url.URL
13− ProfileURL *url.URL
14− ValidateURL *url.URL
15− Scope string
16− ApprovalPrompt string
8+ ProviderName string
9+ ClientID string
10+ ClientSecret string
11+ LoginURL *url.URL
12+ RedeemURL *url.URL
13+ ProfileURL *url.URL
14+ ProtectedResource *url.URL
15+ ValidateURL *url.URL
16+ Scope string
17+ ApprovalPrompt string
1718}
1819
1920func (p *ProviderData) Data() *ProviderData { return p }
providers/provider_default.go+4
@@ -25,6 +25,10 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er
2525 params.Add("client_secret", p.ClientSecret)
2626 params.Add("code", code)
2727 params.Add("grant_type", "authorization_code")
28+ if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
29+ params.Add("resource", p.ProtectedResource.String())
30+ }
31+
2832 var req *http.Request
2933 req, err = http.NewRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
3034 if err != nil {
providers/providers.go+2
@@ -24,6 +24,8 @@ func New(provider string, p *ProviderData) Provider {
2424 return NewLinkedInProvider(p)
2525 case "github":
2626 return NewGitHubProvider(p)
27+ case "azure":
28+ return NewAzureProvider(p)
2729 default:
2830 return NewGoogleProvider(p)
2931 }
watcher.go+2−3
@@ -41,9 +41,8 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) {
4141 for {
4242 select {
4343 case _ = <-done:
44− log.Printf("Shutting down watcher for: %s",
45− filename)
46− return
44+ log.Printf("Shutting down watcher for: %s", filename)
45+ break
4746 case event := <-watcher.Events:
4847 // On Arch Linux, it appears Chmod events precede Remove events,
4948 // which causes a race between action() and the coming Remove event.