git.iko.soy/typeable/oauth2-proxy
on master help sign in
fix(entra-id): use federated credentials for refresh token (#3031)
7d85c99Richard Hagen
CHANGELOG.md+1
@@ -8,6 +8,7 @@
8
8
9## Changes since v7.8.2
9## Changes since v7.8.2
10
10
11+- [#3031](https://github.com/oauth2-proxy/oauth2-proxy/pull/3031) Fixes Refresh Token bug with Entra ID and Workload Identity (#3027)[https://github.com/oauth2-proxy/oauth2-proxy/issues/3028] by using client assertion when redeeming the token (@richard87)
11- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
12- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
12- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
13- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
13
14
providers/ms_entra_id.go+93−18
@@ -8,7 +8,9 @@ import (
8 "net/url"
8 "net/url"
9 "os"
9 "os"
10 "regexp"
10 "regexp"
11+ "time"
11
12
13+ "github.com/coreos/go-oidc/v3/oidc"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
16 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -114,7 +116,8 @@ func (p *MicrosoftEntraIDProvider) redeemWithFederatedToken(ctx context.Context,
114
116
115 params := url.Values{}
117 params := url.Values{}
116
118
117− // create custom exchange parameters
119+ // Exchange parameters for token federation
120+ // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential
118 if codeVerifier != "" {
121 if codeVerifier != "" {
119 params.Add("code_verifier", codeVerifier)
122 params.Add("code_verifier", codeVerifier)
120 }
123 }
@@ -125,29 +128,78 @@ func (p *MicrosoftEntraIDProvider) redeemWithFederatedToken(ctx context.Context,
125 params.Add("code", code)
128 params.Add("code", code)
126 params.Add("grant_type", "authorization_code")
129 params.Add("grant_type", "authorization_code")
127
130
128− // perform exchange
131+ token, err := p.fetchToken(ctx, params)
129− resp := requests.New(p.RedeemURL.String()).
132+ if err != nil {
130− WithContext(ctx).
133+ return nil, fmt.Errorf("error fetching token: %w", err)
131− WithMethod("POST").
134+ }
132− WithBody(bytes.NewBufferString(params.Encode())).
133− SetHeader("Content-Type", "application/x-www-form-urlencoded").
134− Do()
135
135
136− // prepare token of type *oauth2.Token
136+ return p.OIDCProvider.createSession(ctx, token, false)
137− var token *oauth2.Token
137+}
138− var rawResponse interface{}
139
138
140− body := resp.Body()
139+// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
141− if err := json.Unmarshal(body, &rawResponse); err != nil {
140+func (p *MicrosoftEntraIDProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
141+ if s == nil || s.RefreshToken == "" {
142− return nil, err
142+ return false, nil
143 }
143 }
144
144
145− if err := json.Unmarshal(body, &token); err != nil {
145+ var err error
146− return nil, err
146+ ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
147+ if p.federatedTokenAuth {
148+ err = p.redeemRefreshTokenWithFederatedToken(ctx, s)
149+ } else {
150+ err = p.redeemRefreshToken(ctx, s)
147 }
151 }
148
152
149− // create session using new token and generic OIDC provider
153+ if err != nil {
154+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
155+ }
156+
157+ return true, nil
158+}
159+
160+// redeemRefreshTokenWithFederatedToken uses a RefreshToken and federated credentials with the RedeemURL to refresh the
161+// Refresh Token, Access Token and ID Token
162+func (p *MicrosoftEntraIDProvider) redeemRefreshTokenWithFederatedToken(ctx context.Context, s *sessions.SessionState) error {
163+ federatedTokenPath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
164+ federatedToken, err := os.ReadFile(federatedTokenPath)
165+ if err != nil {
166+ return fmt.Errorf("error reading federated token file %s: %s", federatedTokenPath, err)
167+ }
168+
169+ params := url.Values{}
170+ params.Add("client_id", p.ClientID)
171+ params.Add("client_assertion", string(federatedToken))
172+ params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
173+ params.Add("refresh_token", s.RefreshToken)
174+ params.Add("grant_type", "refresh_token")
175+ params.Add("expiry", time.Now().Add(-time.Hour).Format(time.RFC3339))
176+
177+ token, err := p.fetchToken(ctx, params)
178+ if err != nil {
179+ return fmt.Errorf("error fetching token: %w", err)
180+ }
181+
150− return p.OIDCProvider.createSession(ctx, token.WithExtra(rawResponse), false)
182+ newSession, err := p.OIDCProvider.createSession(ctx, token, true)
183+ if err != nil {
184+ return fmt.Errorf("unable create new session state from response: %v", err)
185+ }
186+
187+ // Update the ID Token and user details if returned as part of the refresh response
188+ // ref. https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
189+ if newSession.IDToken != "" {
190+ s.IDToken = newSession.IDToken
191+ s.Email = newSession.Email
192+ s.User = newSession.User
193+ s.Groups = newSession.Groups
194+ s.PreferredUsername = newSession.PreferredUsername
195+ }
196+
197+ s.AccessToken = newSession.AccessToken
198+ s.RefreshToken = newSession.RefreshToken
199+ s.CreatedAt = newSession.CreatedAt
200+ s.ExpiresOn = newSession.ExpiresOn
201+
202+ return nil
151}
203}
152
204
153// checkGroupOverage checks ID token's group membership claims for the group overage
205// checkGroupOverage checks ID token's group membership claims for the group overage
@@ -245,3 +297,26 @@ func (p *MicrosoftEntraIDProvider) checkTenantMatchesTenantList(tenant string, a
245 }
297 }
246 return false
298 return false
247}
299}
300+
301+func (p *MicrosoftEntraIDProvider) fetchToken(ctx context.Context, params url.Values) (*oauth2.Token, error) {
302+ resp := requests.New(p.RedeemURL.String()).
303+ WithContext(ctx).
304+ WithMethod("POST").
305+ WithBody(bytes.NewBufferString(params.Encode())).
306+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
307+ Do()
308+
309+ var token *oauth2.Token
310+ var rawResponse interface{}
311+
312+ body := resp.Body()
313+ if err := json.Unmarshal(body, &rawResponse); err != nil {
314+ return nil, fmt.Errorf("unable to unmarshal raw response body: %w", err)
315+ }
316+
317+ if err := json.Unmarshal(body, &token); err != nil {
318+ return nil, fmt.Errorf("unable to unmarshal token response body: %w", err)
319+ }
320+
321+ return token.WithExtra(rawResponse), nil
322+}