package providers import ( "context" "fmt" "net/url" "strings" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" ) // ADFSProvider represents an ADFS based Identity Provider type ADFSProvider struct { *OIDCProvider skipScope bool // Expose for unit testing oidcEnrichFunc func(context.Context, *sessions.SessionState) error oidcRefreshFunc func(context.Context, *sessions.SessionState) (bool, error) } var _ Provider = (*ADFSProvider)(nil) const ( adfsProviderName = "ADFS" adfsDefaultScope = "openid email profile" adfsUPNClaim = "upn" ) // NewADFSProvider initiates a new ADFSProvider func NewADFSProvider(p *ProviderData, opts options.Provider) *ADFSProvider { p.setProviderDefaults(providerDefaults{ name: adfsProviderName, scope: adfsDefaultScope, }) if p.ProtectedResource != nil && p.ProtectedResource.String() != "" { resource := p.ProtectedResource.String() if !strings.HasSuffix(resource, "/") { resource += "/" } if p.Scope != "" && !strings.HasPrefix(p.Scope, resource) { p.Scope = resource + p.Scope } } oidcProvider := NewOIDCProvider(p, opts.OIDCConfig) return &ADFSProvider{ OIDCProvider: oidcProvider, skipScope: opts.ADFSConfig.SkipScope, oidcEnrichFunc: oidcProvider.EnrichSession, oidcRefreshFunc: oidcProvider.RefreshSession, } } // GetLoginURL Override to double encode the state parameter. If not query params are lost // More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string, extraParams url.Values) string { if !p.SkipNonce { extraParams.Add("nonce", nonce) } loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams) if p.skipScope { q := loginURL.Query() q.Del("scope") loginURL.RawQuery = q.Encode() } return loginURL.String() } // EnrichSession calls the OIDC ProfileURL to backfill any fields missing // from the claims. If Email is missing, falls back to ADFS `upn` claim. func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { err := p.oidcEnrichFunc(ctx, s) if err != nil || s.Email == "" { // OIDC only errors if email is missing return p.fallbackUPN(ctx, s) } return nil } // RefreshSession refreshes via the OIDC implementation. If email is missing, // falls back to ADFS `upn` claim. func (p *ADFSProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) { refreshed, err := p.oidcRefreshFunc(ctx, s) if err != nil || s.Email != "" { return refreshed, err } err = p.fallbackUPN(ctx, s) return refreshed, err } func (p *ADFSProvider) fallbackUPN(_ context.Context, s *sessions.SessionState) error { claims, err := p.getClaimExtractor(s.IDToken, s.AccessToken) if err != nil { return fmt.Errorf("could not extract claims: %v", err) } upn, found, err := claims.GetClaim(adfsUPNClaim) if err != nil { return fmt.Errorf("could not extract %s claim: %v", adfsUPNClaim, err) } if found && fmt.Sprint(upn) != "" { s.Email = fmt.Sprint(upn) } return nil }