1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-02-03 13:21:51 +02:00

Merge pull request #1247 from oauth2-proxy/adfs-default-claims

Use `upn` as EmailClaim throughout ADFSProvider
This commit is contained in:
Joel Speed 2021-12-06 14:24:41 +00:00 committed by GitHub
commit 5933000b86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 33 deletions

View File

@ -8,6 +8,7 @@
## Changes since v7.2.0 ## Changes since v7.2.0
- [#1247](https://github.com/oauth2-proxy/oauth2-proxy/pull/1247) Use `upn` claim consistently in ADFSProvider (@NickMeves)
- [#1447](https://github.com/oauth2-proxy/oauth2-proxy/pull/1447) Fix docker build/push issues found during last release (@JoelSpeed) - [#1447](https://github.com/oauth2-proxy/oauth2-proxy/pull/1447) Fix docker build/push issues found during last release (@JoelSpeed)
- [#1433](https://github.com/oauth2-proxy/oauth2-proxy/pull/1433) Let authentication fail when session validation fails (@stippi2) - [#1433](https://github.com/oauth2-proxy/oauth2-proxy/pull/1433) Let authentication fail when session validation fails (@stippi2)
- [#1445](https://github.com/oauth2-proxy/oauth2-proxy/pull/1445) Fix docker container multi arch build issue by passing GOARCH details to make build (@jkandasa) - [#1445](https://github.com/oauth2-proxy/oauth2-proxy/pull/1445) Fix docker container multi arch build issue by passing GOARCH details to make build (@jkandasa)

View File

@ -2,7 +2,6 @@ package providers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
@ -13,23 +12,27 @@ import (
// ADFSProvider represents an ADFS based Identity Provider // ADFSProvider represents an ADFS based Identity Provider
type ADFSProvider struct { type ADFSProvider struct {
*OIDCProvider *OIDCProvider
SkipScope bool
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) var _ Provider = (*ADFSProvider)(nil)
const ( const (
ADFSProviderName = "ADFS" adfsProviderName = "ADFS"
ADFSDefaultScope = "openid email profile" adfsDefaultScope = "openid email profile"
ADFSSkipScope = false adfsSkipScope = false
adfsUPNClaim = "upn"
) )
// NewADFSProvider initiates a new ADFSProvider // NewADFSProvider initiates a new ADFSProvider
func NewADFSProvider(p *ProviderData) *ADFSProvider { func NewADFSProvider(p *ProviderData) *ADFSProvider {
p.setProviderDefaults(providerDefaults{ p.setProviderDefaults(providerDefaults{
name: ADFSProviderName, name: adfsProviderName,
scope: ADFSDefaultScope, scope: adfsDefaultScope,
}) })
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" { if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
@ -43,18 +46,22 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
} }
} }
return &ADFSProvider{ oidcProvider := &OIDCProvider{
OIDCProvider: &OIDCProvider{
ProviderData: p, ProviderData: p,
SkipNonce: true, SkipNonce: false,
}, }
SkipScope: ADFSSkipScope,
return &ADFSProvider{
OIDCProvider: oidcProvider,
skipScope: adfsSkipScope,
oidcEnrichFunc: oidcProvider.EnrichSession,
oidcRefreshFunc: oidcProvider.RefreshSession,
} }
} }
// Configure defaults the ADFSProvider configuration options // Configure defaults the ADFSProvider configuration options
func (p *ADFSProvider) Configure(skipScope bool) { func (p *ADFSProvider) Configure(skipScope bool) {
p.SkipScope = skipScope p.skipScope = skipScope
} }
// GetLoginURL Override to double encode the state parameter. If not query params are lost // GetLoginURL Override to double encode the state parameter. If not query params are lost
@ -65,7 +72,7 @@ func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
extraParams.Add("nonce", nonce) extraParams.Add("nonce", nonce)
} }
loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams) loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams)
if p.SkipScope { if p.skipScope {
q := loginURL.Query() q := loginURL.Query()
q.Del("scope") q.Del("scope")
loginURL.RawQuery = q.Encode() loginURL.RawQuery = q.Encode()
@ -73,28 +80,40 @@ func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
return loginURL.String() return loginURL.String()
} }
// EnrichSession to add email // 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 { func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
if s.Email != "" { err := p.oidcEnrichFunc(ctx, s)
if err != nil || s.Email == "" {
// OIDC only errors if email is missing
return p.fallbackUPN(ctx, s)
}
return nil 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(ctx context.Context, s *sessions.SessionState) error {
idToken, err := p.Verifier.Verify(ctx, s.IDToken) idToken, err := p.Verifier.Verify(ctx, s.IDToken)
if err != nil { if err != nil {
return err return err
} }
claims, err := p.getClaims(idToken)
p.EmailClaim = "upn"
c, err := p.getClaims(idToken)
if err != nil { if err != nil {
return fmt.Errorf("couldn't extract claims from id_token (%v)", err) return fmt.Errorf("couldn't extract claims from id_token (%v)", err)
} }
s.Email = c.Email upn := claims.raw[adfsUPNClaim]
if upn != nil {
if s.Email == "" { s.Email = fmt.Sprint(upn)
err = errors.New("email not set")
} }
return nil
return err
} }

View File

@ -2,13 +2,17 @@ package providers
import ( import (
"context" "context"
"crypto/rand"
"crypto/rsa"
"encoding/base64" "encoding/base64"
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings" "strings"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/ginkgo/extensions/table"
@ -25,8 +29,18 @@ func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []by
return decodeString, nil return decodeString, nil
} }
func testADFSProvider(hostname string) *ADFSProvider { type adfsClaims struct {
UPN string `json:"upn,omitempty"`
idTokenClaims
}
func newSignedTestADFSToken(tokenClaims adfsClaims) (string, error) {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
return standardClaims.SignedString(key)
}
func testADFSProvider(hostname string) *ADFSProvider {
o := oidc.NewVerifier( o := oidc.NewVerifier(
"https://issuer.example.com", "https://issuer.example.com",
fakeADFSJwks{}, fakeADFSJwks{},
@ -41,6 +55,7 @@ func testADFSProvider(hostname string) *ADFSProvider {
ValidateURL: &url.URL{}, ValidateURL: &url.URL{},
Scope: "", Scope: "",
Verifier: o, Verifier: o,
EmailClaim: OIDCEmailClaim,
}) })
if hostname != "" { if hostname != "" {
@ -54,7 +69,6 @@ func testADFSProvider(hostname string) *ADFSProvider {
} }
func testADFSBackend() *httptest.Server { func testADFSBackend() *httptest.Server {
authResponse := ` authResponse := `
{ {
"access_token": "my_access_token", "access_token": "my_access_token",
@ -129,13 +143,12 @@ var _ = Describe("ADFS Provider Tests", func() {
Context("with valid token", func() { Context("with valid token", func() {
It("should not throw an error", func() { It("should not throw an error", func() {
p.EmailClaim = "email"
rawIDToken, _ := newSignedTestIDToken(defaultIDToken) rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
idToken, err := p.Verifier.Verify(context.Background(), rawIDToken) idToken, err := p.Verifier.Verify(context.Background(), rawIDToken)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
session, err := p.buildSessionFromClaims(idToken) session, err := p.buildSessionFromClaims(idToken)
session.IDToken = rawIDToken
Expect(err).To(BeNil()) Expect(err).To(BeNil())
session.IDToken = rawIDToken
err = p.EnrichSession(context.Background(), session) err = p.EnrichSession(context.Background(), session)
Expect(session.Email).To(Equal("janed@me.com")) Expect(session.Email).To(Equal("janed@me.com"))
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -149,7 +162,7 @@ var _ = Describe("ADFS Provider Tests", func() {
ProtectedResource: resource, ProtectedResource: resource,
Scope: "", Scope: "",
}) })
p.SkipScope = true p.skipScope = true
result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "") result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
Expect(result).NotTo(ContainSubstring("scope=")) Expect(result).NotTo(ContainSubstring("scope="))
@ -202,4 +215,78 @@ var _ = Describe("ADFS Provider Tests", func() {
}), }),
) )
}) })
Context("UPN Fallback", func() {
var idToken string
var session *sessions.SessionState
BeforeEach(func() {
var err error
idToken, err = newSignedTestADFSToken(adfsClaims{
UPN: "upn@company.com",
idTokenClaims: minimalIDToken,
})
Expect(err).ToNot(HaveOccurred())
session = &sessions.SessionState{
IDToken: idToken,
}
})
Describe("EnrichSession", func() {
It("uses email claim if present", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
s.Email = "person@company.com"
return nil
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("person@company.com"))
})
It("falls back to UPN claim if Email is missing", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
return nil
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
It("falls back to UPN claim on errors", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
return errors.New("neither the id_token nor the profileURL set an email")
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
})
Describe("RefreshSession", func() {
It("uses email claim if present", func() {
p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
s.Email = "person@company.com"
return true, nil
}
_, err := p.RefreshSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("person@company.com"))
})
It("falls back to UPN claim if Email is missing", func() {
p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
return true, nil
}
_, err := p.RefreshSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
})
})
}) })