@@ -1,9 +1,12 @@
11package providers
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
57 "fmt"
68 "net/url"
9+ "os"
710 "regexp"
811
912 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@@ -12,12 +15,14 @@ import (
1215 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
1316 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
1417 "github.com/spf13/cast"
18+ "golang.org/x/oauth2"
1519)
1620
1721// MicrosoftEntraIDProvider represents provider for Azure Entra Authentication V2 endpoint
1822type MicrosoftEntraIDProvider struct {
1923 *OIDCProvider
2024 multiTenantAllowedTenants []string
25+ federatedTokenAuth bool
2126
2227 microsoftGraphURL *url.URL
2328}
@@ -44,6 +49,7 @@ func NewMicrosoftEntraIDProvider(p *ProviderData, opts options.Provider) *Micros
4449 OIDCProvider: NewOIDCProvider(p, opts.OIDCConfig),
4550
4651 multiTenantAllowedTenants: opts.MicrosoftEntraIDConfig.AllowedTenants,
52+ federatedTokenAuth: opts.MicrosoftEntraIDConfig.FederatedTokenAuth,
4753 microsoftGraphURL: microsoftGraphURL,
4854 }
4955}
@@ -89,6 +95,61 @@ func (p *MicrosoftEntraIDProvider) ValidateSession(ctx context.Context, session
8995 return p.OIDCProvider.ValidateSession(ctx, session)
9096}
9197
98+// Redeem exchanges the OAuth2 authentication token for an ID token, considering federated token authentication
99+func (p *MicrosoftEntraIDProvider) Redeem(ctx context.Context, redirectURL, code, codeVerifier string) (*sessions.SessionState, error) {
100+ if p.federatedTokenAuth {
101+ return p.redeemWithFederatedToken(ctx, redirectURL, code, codeVerifier)
102+ }
103+
104+ return p.OIDCProvider.Redeem(ctx, redirectURL, code, codeVerifier)
105+}
106+
107+// redeemWithFederatedToken performs custom token exchange with federated token instead of client secret
108+func (p *MicrosoftEntraIDProvider) redeemWithFederatedToken(ctx context.Context, redirectURL, code, codeVerifier string) (*sessions.SessionState, error) {
109+ federatedTokenPath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
110+ federatedToken, err := os.ReadFile(federatedTokenPath)
111+ if err != nil {
112+ return nil, fmt.Errorf("error reading federated token file %s: %s", federatedTokenPath, err)
113+ }
114+
115+ params := url.Values{}
116+
117+ // create custom exchange parameters
118+ if codeVerifier != "" {
119+ params.Add("code_verifier", codeVerifier)
120+ }
121+ params.Add("redirect_uri", redirectURL)
122+ params.Add("client_id", p.ClientID)
123+ params.Add("client_assertion", string(federatedToken))
124+ params.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
125+ params.Add("code", code)
126+ params.Add("grant_type", "authorization_code")
127+
128+ // perform exchange
129+ resp := requests.New(p.RedeemURL.String()).
130+ WithContext(ctx).
131+ WithMethod("POST").
132+ WithBody(bytes.NewBufferString(params.Encode())).
133+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
134+ Do()
135+
136+ // prepare token of type *oauth2.Token
137+ var token *oauth2.Token
138+ var rawResponse interface{}
139+
140+ body := resp.Body()
141+ if err := json.Unmarshal(body, &rawResponse); err != nil {
142+ return nil, err
143+ }
144+
145+ if err := json.Unmarshal(body, &token); err != nil {
146+ return nil, err
147+ }
148+
149+ // create session using new token and generic OIDC provider
150+ return p.OIDCProvider.createSession(ctx, token.WithExtra(rawResponse), false)
151+}
152+
92153// checkGroupOverage checks ID token's group membership claims for the group overage
93154func (p *MicrosoftEntraIDProvider) checkGroupOverage(session *sessions.SessionState) (bool, error) {
94155 extractor, err := p.getClaimExtractor(session.IDToken, session.AccessToken)