fix(entra-id): use federated credentials for refresh token (#3031)
* fix: use federated credentials to refresh token in entra id * fix: add some error handling * chore: update changelog * chore: update comments * chore: update comments * doc: reference entra id docs and clearer phrasing of comments Signed-off-by: Jan Larwig <jan@larwig.com> --------- Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
		
							parent
							
								
									3afae76103
								
							
						
					
					
						commit
						7d85c99d8e
					
				|  | @ -8,6 +8,7 @@ | ||||||
| 
 | 
 | ||||||
| ## Changes since v7.8.2 | ## Changes since v7.8.2 | ||||||
| 
 | 
 | ||||||
|  | - [#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) | ||||||
| - [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it) | - [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it) | ||||||
| - [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer) | - [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,9 @@ import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/coreos/go-oidc/v3/oidc" | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | ||||||
|  | @ -114,7 +116,8 @@ func (p *MicrosoftEntraIDProvider) redeemWithFederatedToken(ctx context.Context, | ||||||
| 
 | 
 | ||||||
| 	params := url.Values{} | 	params := url.Values{} | ||||||
| 
 | 
 | ||||||
| 	// create custom exchange parameters
 | 	// Exchange parameters for token federation
 | ||||||
|  | 	// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential
 | ||||||
| 	if codeVerifier != "" { | 	if codeVerifier != "" { | ||||||
| 		params.Add("code_verifier", codeVerifier) | 		params.Add("code_verifier", codeVerifier) | ||||||
| 	} | 	} | ||||||
|  | @ -125,29 +128,78 @@ func (p *MicrosoftEntraIDProvider) redeemWithFederatedToken(ctx context.Context, | ||||||
| 	params.Add("code", code) | 	params.Add("code", code) | ||||||
| 	params.Add("grant_type", "authorization_code") | 	params.Add("grant_type", "authorization_code") | ||||||
| 
 | 
 | ||||||
| 	// perform exchange
 | 	token, err := p.fetchToken(ctx, params) | ||||||
| 	resp := requests.New(p.RedeemURL.String()). | 	if err != nil { | ||||||
| 		WithContext(ctx). | 		return nil, fmt.Errorf("error fetching token: %w", err) | ||||||
| 		WithMethod("POST"). |  | ||||||
| 		WithBody(bytes.NewBufferString(params.Encode())). |  | ||||||
| 		SetHeader("Content-Type", "application/x-www-form-urlencoded"). |  | ||||||
| 		Do() |  | ||||||
| 
 |  | ||||||
| 	// prepare token of type *oauth2.Token
 |  | ||||||
| 	var token *oauth2.Token |  | ||||||
| 	var rawResponse interface{} |  | ||||||
| 
 |  | ||||||
| 	body := resp.Body() |  | ||||||
| 	if err := json.Unmarshal(body, &rawResponse); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := json.Unmarshal(body, &token); err != nil { | 	return p.OIDCProvider.createSession(ctx, token, false) | ||||||
| 		return nil, err |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 	// create session using new token and generic OIDC provider
 | // RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
 | ||||||
| 	return p.OIDCProvider.createSession(ctx, token.WithExtra(rawResponse), false) | func (p *MicrosoftEntraIDProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) { | ||||||
|  | 	if s == nil || s.RefreshToken == "" { | ||||||
|  | 		return false, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var err error | ||||||
|  | 	ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient) | ||||||
|  | 	if p.federatedTokenAuth { | ||||||
|  | 		err = p.redeemRefreshTokenWithFederatedToken(ctx, s) | ||||||
|  | 	} else { | ||||||
|  | 		err = p.redeemRefreshToken(ctx, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, fmt.Errorf("unable to redeem refresh token: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // redeemRefreshTokenWithFederatedToken uses a RefreshToken and federated credentials with the RedeemURL to refresh the
 | ||||||
|  | // Refresh Token, Access Token and ID Token
 | ||||||
|  | func (p *MicrosoftEntraIDProvider) redeemRefreshTokenWithFederatedToken(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | 	federatedTokenPath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE") | ||||||
|  | 	federatedToken, err := os.ReadFile(federatedTokenPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error reading federated token file %s: %s", federatedTokenPath, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	params := url.Values{} | ||||||
|  | 	params.Add("client_id", p.ClientID) | ||||||
|  | 	params.Add("client_assertion", string(federatedToken)) | ||||||
|  | 	params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") | ||||||
|  | 	params.Add("refresh_token", s.RefreshToken) | ||||||
|  | 	params.Add("grant_type", "refresh_token") | ||||||
|  | 	params.Add("expiry", time.Now().Add(-time.Hour).Format(time.RFC3339)) | ||||||
|  | 
 | ||||||
|  | 	token, err := p.fetchToken(ctx, params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error fetching token: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	newSession, err := p.OIDCProvider.createSession(ctx, token, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("unable create new session state from response: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update the ID Token and user details if returned as part of the refresh response
 | ||||||
|  | 	// ref. https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
 | ||||||
|  | 	if newSession.IDToken != "" { | ||||||
|  | 		s.IDToken = newSession.IDToken | ||||||
|  | 		s.Email = newSession.Email | ||||||
|  | 		s.User = newSession.User | ||||||
|  | 		s.Groups = newSession.Groups | ||||||
|  | 		s.PreferredUsername = newSession.PreferredUsername | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	s.AccessToken = newSession.AccessToken | ||||||
|  | 	s.RefreshToken = newSession.RefreshToken | ||||||
|  | 	s.CreatedAt = newSession.CreatedAt | ||||||
|  | 	s.ExpiresOn = newSession.ExpiresOn | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // checkGroupOverage checks ID token's group membership claims for the group overage
 | // checkGroupOverage checks ID token's group membership claims for the group overage
 | ||||||
|  | @ -245,3 +297,26 @@ func (p *MicrosoftEntraIDProvider) checkTenantMatchesTenantList(tenant string, a | ||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *MicrosoftEntraIDProvider) fetchToken(ctx context.Context, params url.Values) (*oauth2.Token, error) { | ||||||
|  | 	resp := requests.New(p.RedeemURL.String()). | ||||||
|  | 		WithContext(ctx). | ||||||
|  | 		WithMethod("POST"). | ||||||
|  | 		WithBody(bytes.NewBufferString(params.Encode())). | ||||||
|  | 		SetHeader("Content-Type", "application/x-www-form-urlencoded"). | ||||||
|  | 		Do() | ||||||
|  | 
 | ||||||
|  | 	var token *oauth2.Token | ||||||
|  | 	var rawResponse interface{} | ||||||
|  | 
 | ||||||
|  | 	body := resp.Body() | ||||||
|  | 	if err := json.Unmarshal(body, &rawResponse); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to unmarshal raw response body: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := json.Unmarshal(body, &token); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to unmarshal token response body: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return token.WithExtra(rawResponse), nil | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue