git.iko.soy/typeable/oauth2-proxy
on master help sign in
Azure token refresh (#754)

* Implement azure token refresh Based on original PR https://github.com/oauth2-proxy/oauth2-proxy/pull/278 * Update CHANGELOG.md * Apply suggestions from code review Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Set CreatedAt to Now() on token refresh Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commit0e119d7c84b165f04b31bb74fbfc2c96f8269a02 authorAlexander Block <ablock84@gmail.com> dateWed 4 Nov 2020, 20:25 (6 years ago) parent65016c8
3 files changed+121−2
CHANGELOG.md+7 providers/azure.go+66 providers/azure_test.go+48−2
CHANGELOG.md+7
@@ -23,9 +23,16 @@
2323 via the login url. If this option was used in the past, behavior will change with this release as it will
2424 affect the tokens returned by Azure. In the past, the tokens were always for `https://graph.microsoft.com` (the default)
2525 and will now be for the configured resource (if it exists, otherwise it will run into errors)
26+- [#754](https://github.com/oauth2-proxy/oauth2-proxy/pull/754) The Azure provider now has token refresh functionality implemented. This means that there won't
27+ be any redirects in the browser anymore when tokens expire, but instead a token refresh is initiated
28+ in the background, which leads to new tokens being returned in the cookies.
29+ - Please note that `--cookie-refresh` must be 0 (the default) or equal to the token lifespan configured in Azure AD to make
30+ Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation
31+ to decide if a refresh is required.
2632
2733## Changes since v6.1.1
2834
35+- [#754](https://github.com/oauth2-proxy/oauth2-proxy/pull/754) Azure token refresh (@codablock)
2936- [#825](https://github.com/oauth2-proxy/oauth2-proxy/pull/825) Fix code coverage reporting on GitHub actions(@JoelSpeed)
3037- [#796](https://github.com/oauth2-proxy/oauth2-proxy/pull/796) Deprecate GetUserName & GetEmailAdress for EnrichSessionState (@NickMeves)
3138- [#705](https://github.com/oauth2-proxy/oauth2-proxy/pull/705) Add generic Header injectors for upstream request and response headers (@JoelSpeed)
providers/azure.go+66
@@ -4,6 +4,7 @@ import (
44 "bytes"
55 "context"
66 "errors"
7+ "fmt"
78 "net/http"
89 "net/url"
910 "time"
@@ -74,6 +75,9 @@ func NewAzureProvider(p *ProviderData) *AzureProvider {
7475 if p.ProtectedResource == nil || p.ProtectedResource.String() == "" {
7576 p.ProtectedResource = azureDefaultProtectResourceURL
7677 }
78+ if p.ValidateURL == nil || p.ValidateURL.String() == "" {
79+ p.ValidateURL = p.ProfileURL
80+ }
7781
7882 return &AzureProvider{
7983 ProviderData: p,
@@ -103,6 +107,7 @@ func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
103107 }
104108}
105109
110+// Redeem exchanges the OAuth2 authentication token for an ID token
106111func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
107112 if code == "" {
108113 err = errors.New("missing code")
@@ -123,6 +128,7 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (s
123128 params.Add("resource", p.ProtectedResource.String())
124129 }
125130
131+ // blindly try json and x-www-form-urlencoded
126132 var jsonResponse struct {
127133 AccessToken string `json:"access_token"`
128134 RefreshToken string `json:"refresh_token"`
@@ -151,6 +157,61 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (s
151157 RefreshToken: jsonResponse.RefreshToken,
152158 }
153159 return
160+
161+}
162+
163+// RefreshSessionIfNeeded checks if the session has expired and uses the
164+// RefreshToken to fetch a new ID token if required
165+func (p *AzureProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
166+ if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
167+ return false, nil
168+ }
169+
170+ origExpiration := s.ExpiresOn
171+
172+ err := p.redeemRefreshToken(ctx, s)
173+ if err != nil {
174+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
175+ }
176+
177+ fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
178+ return true, nil
179+}
180+
181+func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
182+ params := url.Values{}
183+ params.Add("client_id", p.ClientID)
184+ params.Add("client_secret", p.ClientSecret)
185+ params.Add("refresh_token", s.RefreshToken)
186+ params.Add("grant_type", "refresh_token")
187+
188+ var jsonResponse struct {
189+ AccessToken string `json:"access_token"`
190+ RefreshToken string `json:"refresh_token"`
191+ ExpiresOn int64 `json:"expires_on,string"`
192+ IDToken string `json:"id_token"`
193+ }
194+
195+ err = requests.New(p.RedeemURL.String()).
196+ WithContext(ctx).
197+ WithMethod("POST").
198+ WithBody(bytes.NewBufferString(params.Encode())).
199+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
200+ Do().
201+ UnmarshalInto(&jsonResponse)
202+
203+ if err != nil {
204+ return
205+ }
206+
207+ now := time.Now()
208+ expires := time.Unix(jsonResponse.ExpiresOn, 0)
209+ s.AccessToken = jsonResponse.AccessToken
210+ s.IDToken = jsonResponse.IDToken
211+ s.RefreshToken = jsonResponse.RefreshToken
212+ s.CreatedAt = &now
213+ s.ExpiresOn = &expires
214+ return
154215}
155216
156217func makeAzureHeader(accessToken string) http.Header {
@@ -219,3 +280,8 @@ func (p *AzureProvider) GetLoginURL(redirectURI, state string) string {
219280 a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
220281 return a.String()
221282}
283+
284+// ValidateSessionState validates the AccessToken
285+func (p *AzureProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
286+ return validateToken(ctx, p, s.AccessToken, makeAzureHeader(s.AccessToken))
287+}
providers/azure_test.go+48−2
@@ -8,6 +8,8 @@ import (
88 "testing"
99 "time"
1010
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+
1113 . "github.com/onsi/gomega"
1214 "github.com/stretchr/testify/assert"
1315)
@@ -42,7 +44,7 @@ func TestNewAzureProvider(t *testing.T) {
4244 g.Expect(providerData.LoginURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/authorize"))
4345 g.Expect(providerData.RedeemURL.String()).To(Equal("https://login.microsoftonline.com/common/oauth2/token"))
4446 g.Expect(providerData.ProfileURL.String()).To(Equal("https://graph.microsoft.com/v1.0/me"))
45− g.Expect(providerData.ValidateURL.String()).To(Equal(""))
47+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://graph.microsoft.com/v1.0/me"))
4648 g.Expect(providerData.Scope).To(Equal("openid"))
4749}
4850
@@ -97,7 +99,7 @@ func TestAzureSetTenant(t *testing.T) {
9799 p.Data().ProfileURL.String())
98100 assert.Equal(t, "https://graph.microsoft.com",
99101 p.Data().ProtectedResource.String())
100− assert.Equal(t, "", p.Data().ValidateURL.String())
102+ assert.Equal(t, "https://graph.microsoft.com/v1.0/me", p.Data().ValidateURL.String())
101103 assert.Equal(t, "openid", p.Data().Scope)
102104}
103105
@@ -220,3 +222,47 @@ func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
220222 result := p.GetLoginURL("https://my.test.app/oauth", "")
221223 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
222224}
225+
226+func TestAzureProviderGetsTokensInRedeem(t *testing.T) {
227+ b := testAzureBackend(`{ "access_token": "some_access_token", "refresh_token": "some_refresh_token", "expires_on": "1136239445", "id_token": "some_id_token" }`)
228+ defer b.Close()
229+ timestamp, _ := time.Parse(time.RFC3339, "2006-01-02T22:04:05Z")
230+ bURL, _ := url.Parse(b.URL)
231+ p := testAzureProvider(bURL.Host)
232+
233+ session, err := p.Redeem(context.Background(), "http://redirect/", "code1234")
234+ assert.Equal(t, nil, err)
235+ assert.NotEqual(t, session, nil)
236+ assert.Equal(t, "some_access_token", session.AccessToken)
237+ assert.Equal(t, "some_refresh_token", session.RefreshToken)
238+ assert.Equal(t, "some_id_token", session.IDToken)
239+ assert.Equal(t, timestamp, session.ExpiresOn.UTC())
240+}
241+
242+func TestAzureProviderNotRefreshWhenNotExpired(t *testing.T) {
243+ p := testAzureProvider("")
244+
245+ expires := time.Now().Add(time.Duration(1) * time.Hour)
246+ session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: "some_refresh_token", IDToken: "some_id_token", ExpiresOn: &expires}
247+ refreshNeeded, err := p.RefreshSessionIfNeeded(context.Background(), session)
248+ assert.Equal(t, nil, err)
249+ assert.False(t, refreshNeeded)
250+}
251+
252+func TestAzureProviderRefreshWhenExpired(t *testing.T) {
253+ b := testAzureBackend(`{ "access_token": "new_some_access_token", "refresh_token": "new_some_refresh_token", "expires_on": "32693148245", "id_token": "new_some_id_token" }`)
254+ defer b.Close()
255+ timestamp, _ := time.Parse(time.RFC3339, "3006-01-02T22:04:05Z")
256+ bURL, _ := url.Parse(b.URL)
257+ p := testAzureProvider(bURL.Host)
258+
259+ expires := time.Now().Add(time.Duration(-1) * time.Hour)
260+ session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: "some_refresh_token", IDToken: "some_id_token", ExpiresOn: &expires}
261+ _, err := p.RefreshSessionIfNeeded(context.Background(), session)
262+ assert.Equal(t, nil, err)
263+ assert.NotEqual(t, session, nil)
264+ assert.Equal(t, "new_some_access_token", session.AccessToken)
265+ assert.Equal(t, "new_some_refresh_token", session.RefreshToken)
266+ assert.Equal(t, "new_some_id_token", session.IDToken)
267+ assert.Equal(t, timestamp, session.ExpiresOn.UTC())
268+}