Skip to content
This repository was archived by the owner on Jan 24, 2019. It is now read-only.

Commit 10f47e3

Browse files
eelcocramerjehiah
authored andcommitted
Add Azure Provider
1 parent d5a332c commit 10f47e3

9 files changed

+272
-19
lines changed

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ You will need to register an OAuth application with a Provider (Google, Github o
2929
Valid 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

7779
Note: 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

8192
1. Create a new project: https://github.com/settings/developers
@@ -102,6 +113,12 @@ For LinkedIn, the registration steps are:
102113

103114
The [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

107124
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
120137
Usage 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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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.go

+86
Original file line numberDiff line numberDiff line change
@@ -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.go

+135
Original file line numberDiff line numberDiff line change
@@ -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": "[email protected]" }`)
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, "[email protected]", email)
135+
}

providers/provider_data.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import (
55
)
66

77
type 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

1920
func (p *ProviderData) Data() *ProviderData { return p }

providers/provider_default.go

+4
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -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.

0 commit comments

Comments
 (0)