Repository: pusher/oauth2_proxy
Pull request #258 (2019-08-29 14:14:39)

Add IDToken for Azure provider
<!--- Provide a general summary of your changes in the Title above -->

## Description

This adds the IDToken to the session for the Azure provider.

## Motivation and Context

This allows it to be passed as a bearer token in the Authorization header to an upstream such as the Kubernetes dashboard. It solves the issue in https://github.com/pusher/oauth2_proxy/issues/131
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

## How Has This Been Tested?

This has been tested against Azure AD to check that it returns the IDToken. It was tested with nginx in auth proxy mode to check that this token could be used to authenticate to the kubernetes dashboard.

As a result of the change the cookie being returned is larger and has to be split. This can cause an issue if using nginx to proxy requiring proxy buffer size be increased (see https://andrewlock.net/fixing-nginx-upstream-sent-too-big-header-error-when-running-an-ingress-controller-in-kubernetes/). I'm not sure therefore if something should be noted in the changelog regarding this.
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My change requires a change to the documentation or CHANGELOG.
- [x] I have updated the documentation/CHANGELOG accordingly.
- [X] I have created a feature (non-master) branch for my PR.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d76e8958..782278e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
 - [#227](https://github.com/pusher/oauth2_proxy/pull/227) Add Keycloak provider (@Ofinka)
 - [#273](https://github.com/pusher/oauth2_proxy/pull/273) Support Go 1.13 (@dio)
 - [#275](https://github.com/pusher/oauth2_proxy/pull/275) docker: build from debian buster (@syscll)
+- [#258](https://github.com/pusher/oauth2_proxy/pull/258) Add IDToken for Azure provider
+  - This PR adds the IDToken into the session for the Azure provider allowing requests to a backend to be identified as a specific user. As a consequence, if you are using a cookie to store the session the cookie will now exceed the 4kb size limit and be split into multiple cookies. This can cause problems when using nginx as a proxy, resulting in no cookie being passed at all. Either increase the proxy_buffer_size in nginx or implement the redis session storage (see https://pusher.github.io/oauth2_proxy/configuration#redis-storage)
 
 # v4.0.0
 
diff --git a/docs/2_auth.md b/docs/2_auth.md
index 04e63292..991bbae2 100644
--- a/docs/2_auth.md
+++ b/docs/2_auth.md
@@ -81,6 +81,8 @@ Note: The user is checked against the group members list on initial authenticati
    --client-secret=<value from step 6>
 ```
 
+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](configuration#redis-storage) should resolve this.
+
 ### Facebook Auth Provider
 
 1.  Create a new FB App from <https://developers.facebook.com/>
diff --git a/providers/azure.go b/providers/azure.go
index 653090b0..48cea846 100644
--- a/providers/azure.go
+++ b/providers/azure.go
@@ -1,10 +1,14 @@
 package providers
 
 import (
+	"bytes"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/url"
+	"time"
 
 	"github.com/bitly/go-simplejson"
 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
@@ -65,6 +69,67 @@ func (p *AzureProvider) Configure(tenant string) {
 	}
 }
 
+func (p *AzureProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
+	if code == "" {
+		err = errors.New("missing code")
+		return
+	}
+
+	params := url.Values{}
+	params.Add("redirect_uri", redirectURL)
+	params.Add("client_id", p.ClientID)
+	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 {
+		return
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	var resp *http.Response
+	resp, err = http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	var body []byte
+	body, err = ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		return
+	}
+
+	if resp.StatusCode != 200 {
+		err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
+		return
+	}
+
+	var jsonResponse struct {
+		AccessToken  string `json:"access_token"`
+		RefreshToken string `json:"refresh_token"`
+		ExpiresOn    int64  `json:"expires_on,string"`
+		IDToken      string `json:"id_token"`
+	}
+	err = json.Unmarshal(body, &jsonResponse)
+	if err != nil {
+		return
+	}
+
+	s = &sessions.SessionState{
+		AccessToken:  jsonResponse.AccessToken,
+		IDToken:      jsonResponse.IDToken,
+		CreatedAt:    time.Now(),
+		ExpiresOn:    time.Unix(jsonResponse.ExpiresOn, 0),
+		RefreshToken: jsonResponse.RefreshToken,
+	}
+	return
+}
+
 func getAzureHeader(accessToken string) http.Header {
 	header := make(http.Header)
 	header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))


Test patch:
diff --git a/providers/azure_test.go b/providers/azure_test.go
index 8d34bdc8..24745f3a 100644
--- a/providers/azure_test.go
+++ b/providers/azure_test.go
@@ -5,6 +5,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"testing"
+	"time"
 
 	"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
 	"github.com/stretchr/testify/assert"
@@ -20,6 +21,7 @@ func testAzureProvider(hostname string) *AzureProvider {
 			ValidateURL:       &url.URL{},
 			ProtectedResource: &url.URL{},
 			Scope:             ""})
+
 	if hostname != "" {
 		updateURL(p.Data().LoginURL, hostname)
 		updateURL(p.Data().RedeemURL, hostname)
@@ -111,8 +113,11 @@ func testAzureBackend(payload string) *httptest.Server {
 
 	return httptest.NewServer(http.HandlerFunc(
 		func(w http.ResponseWriter, r *http.Request) {
-			if r.URL.Path != path || r.URL.RawQuery != query {
+			if (r.URL.Path != path || r.URL.RawQuery != query) && r.Method != "POST" {
 				w.WriteHeader(404)
+			} else if r.Method == "POST" && r.Body != nil {
+				w.WriteHeader(200)
+				w.Write([]byte(payload))
 			} else if r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
 				w.WriteHeader(403)
 			} else {
@@ -199,3 +204,19 @@ func TestAzureProviderGetEmailAddressIncorrectOtherMails(t *testing.T) {
 	assert.Equal(t, "type assertion to string failed", err.Error())
 	assert.Equal(t, "", email)
 }
+
+func TestAzureProviderRedeemReturnsIdToken(t *testing.T) {
+	b := testAzureBackend(`{ "id_token": "testtoken1234", "expires_on": "1136239445", "refresh_token": "refresh1234" }`)
+	defer b.Close()
+	timestamp, err := time.Parse(time.RFC3339, "2006-01-02T22:04:05Z")
+	assert.Equal(t, nil, err)
+
+	bURL, _ := url.Parse(b.URL)
+	p := testAzureProvider(bURL.Host)
+	p.Data().RedeemURL.Path = "/common/oauth2/token"
+	s, err := p.Redeem("https://localhost", "1234")
+	assert.Equal(t, nil, err)
+	assert.Equal(t, "testtoken1234", s.IDToken)
+	assert.Equal(t, timestamp, s.ExpiresOn.UTC())
+	assert.Equal(t, "refresh1234", s.RefreshToken)
+}