git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #1247 from oauth2-proxy/adfs-default-claims

Use `upn` as EmailClaim throughout ADFSProvider

commit5933000b86259d903c5c13a396e94b43820426c4 authorJoel Speed <Joel.speed@hotmail.co.uk> dateMon 6 Dec 2021, 14:24 (5 years ago) parent1b335a00fa8fca
3 files changed+140−33
CHANGELOG.md+1 providers/adfs.go+47−28 providers/adfs_test.go+92−5
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v7.2.0
1010
11+- [#1247](https://github.com/oauth2-proxy/oauth2-proxy/pull/1247) Use `upn` claim consistently in ADFSProvider (@NickMeves)
1112- [#1447](https://github.com/oauth2-proxy/oauth2-proxy/pull/1447) Fix docker build/push issues found during last release (@JoelSpeed)
1213- [#1433](https://github.com/oauth2-proxy/oauth2-proxy/pull/1433) Let authentication fail when session validation fails (@stippi2)
1314- [#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)
providers/adfs.go+47−28
@@ -2,7 +2,6 @@ package providers
22
33import (
44 "context"
5− "errors"
65 "fmt"
76 "net/url"
87 "strings"
@@ -13,23 +12,27 @@ import (
1312// ADFSProvider represents an ADFS based Identity Provider
1413type ADFSProvider struct {
1514 *OIDCProvider
16− SkipScope bool
15+
16+ skipScope bool
17+ // Expose for unit testing
18+ oidcEnrichFunc func(context.Context, *sessions.SessionState) error
19+ oidcRefreshFunc func(context.Context, *sessions.SessionState) (bool, error)
1720}
1821
1922var _ Provider = (*ADFSProvider)(nil)
2023
2124const (
22− ADFSProviderName = "ADFS"
23− ADFSDefaultScope = "openid email profile"
24− ADFSSkipScope = false
25+ adfsProviderName = "ADFS"
26+ adfsDefaultScope = "openid email profile"
27+ adfsSkipScope = false
28+ adfsUPNClaim = "upn"
2529)
2630
2731// NewADFSProvider initiates a new ADFSProvider
2832func NewADFSProvider(p *ProviderData) *ADFSProvider {
29−
3033 p.setProviderDefaults(providerDefaults{
31− name: ADFSProviderName,
32− scope: ADFSDefaultScope,
34+ name: adfsProviderName,
35+ scope: adfsDefaultScope,
3336 })
3437
3538 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
@@ -43,18 +46,22 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
4346 }
4447 }
4548
49+ oidcProvider := &OIDCProvider{
50+ ProviderData: p,
51+ SkipNonce: false,
52+ }
53+
4654 return &ADFSProvider{
47− OIDCProvider: &OIDCProvider{
48− ProviderData: p,
49− SkipNonce: true,
50− },
51− SkipScope: ADFSSkipScope,
55+ OIDCProvider: oidcProvider,
56+ skipScope: adfsSkipScope,
57+ oidcEnrichFunc: oidcProvider.EnrichSession,
58+ oidcRefreshFunc: oidcProvider.RefreshSession,
5259 }
5360}
5461
5562// Configure defaults the ADFSProvider configuration options
5663func (p *ADFSProvider) Configure(skipScope bool) {
57− p.SkipScope = skipScope
64+ p.skipScope = skipScope
5865}
5966
6067// 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 {
6572 extraParams.Add("nonce", nonce)
6673 }
6774 loginURL := makeLoginURL(p.Data(), redirectURI, url.QueryEscape(state), extraParams)
68− if p.SkipScope {
75+ if p.skipScope {
6976 q := loginURL.Query()
7077 q.Del("scope")
7178 loginURL.RawQuery = q.Encode()
@@ -73,28 +80,40 @@ func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
7380 return loginURL.String()
7481}
7582
76−// EnrichSession to add email
83+// EnrichSession calls the OIDC ProfileURL to backfill any fields missing
84+// from the claims. If Email is missing, falls back to ADFS `upn` claim.
7785func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
78− if s.Email != "" {
79− return nil
86+ err := p.oidcEnrichFunc(ctx, s)
87+ if err != nil || s.Email == "" {
88+ // OIDC only errors if email is missing
89+ return p.fallbackUPN(ctx, s)
90+ }
91+ return nil
92+}
93+
94+// RefreshSession refreshes via the OIDC implementation. If email is missing,
95+// falls back to ADFS `upn` claim.
96+func (p *ADFSProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
97+ refreshed, err := p.oidcRefreshFunc(ctx, s)
98+ if err != nil || s.Email != "" {
99+ return refreshed, err
80100 }
101+ err = p.fallbackUPN(ctx, s)
102+ return refreshed, err
103+}
81104
105+func (p *ADFSProvider) fallbackUPN(ctx context.Context, s *sessions.SessionState) error {
82106 idToken, err := p.Verifier.Verify(ctx, s.IDToken)
83107 if err != nil {
84108 return err
85109 }
86−
87− p.EmailClaim = "upn"
88− c, err := p.getClaims(idToken)
89−
110+ claims, err := p.getClaims(idToken)
90111 if err != nil {
91112 return fmt.Errorf("couldn't extract claims from id_token (%v)", err)
92113 }
93− s.Email = c.Email
94−
95− if s.Email == "" {
96− err = errors.New("email not set")
114+ upn := claims.raw[adfsUPNClaim]
115+ if upn != nil {
116+ s.Email = fmt.Sprint(upn)
97117 }
98−
99− return err
118+ return nil
100119}
providers/adfs_test.go+92−5
@@ -2,13 +2,17 @@ package providers
22
33import (
44 "context"
5+ "crypto/rand"
6+ "crypto/rsa"
57 "encoding/base64"
8+ "errors"
69 "net/http"
710 "net/http/httptest"
811 "net/url"
912 "strings"
1013
1114 "github.com/coreos/go-oidc/v3/oidc"
15+ "github.com/golang-jwt/jwt"
1216 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1317 . "github.com/onsi/ginkgo"
1418 . "github.com/onsi/ginkgo/extensions/table"
@@ -25,8 +29,18 @@ func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []by
2529 return decodeString, nil
2630}
2731
28−func testADFSProvider(hostname string) *ADFSProvider {
32+type adfsClaims struct {
33+ UPN string `json:"upn,omitempty"`
34+ idTokenClaims
35+}
2936
37+func newSignedTestADFSToken(tokenClaims adfsClaims) (string, error) {
38+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
39+ standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
40+ return standardClaims.SignedString(key)
41+}
42+
43+func testADFSProvider(hostname string) *ADFSProvider {
3044 o := oidc.NewVerifier(
3145 "https://issuer.example.com",
3246 fakeADFSJwks{},
@@ -41,6 +55,7 @@ func testADFSProvider(hostname string) *ADFSProvider {
4155 ValidateURL: &url.URL{},
4256 Scope: "",
4357 Verifier: o,
58+ EmailClaim: OIDCEmailClaim,
4459 })
4560
4661 if hostname != "" {
@@ -54,7 +69,6 @@ func testADFSProvider(hostname string) *ADFSProvider {
5469}
5570
5671func testADFSBackend() *httptest.Server {
57−
5872 authResponse := `
5973 {
6074 "access_token": "my_access_token",
@@ -129,13 +143,12 @@ var _ = Describe("ADFS Provider Tests", func() {
129143
130144 Context("with valid token", func() {
131145 It("should not throw an error", func() {
132− p.EmailClaim = "email"
133146 rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
134147 idToken, err := p.Verifier.Verify(context.Background(), rawIDToken)
135148 Expect(err).To(BeNil())
136149 session, err := p.buildSessionFromClaims(idToken)
137− session.IDToken = rawIDToken
138150 Expect(err).To(BeNil())
151+ session.IDToken = rawIDToken
139152 err = p.EnrichSession(context.Background(), session)
140153 Expect(session.Email).To(Equal("janed@me.com"))
141154 Expect(err).To(BeNil())
@@ -149,7 +162,7 @@ var _ = Describe("ADFS Provider Tests", func() {
149162 ProtectedResource: resource,
150163 Scope: "",
151164 })
152− p.SkipScope = true
165+ p.skipScope = true
153166
154167 result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
155168 Expect(result).NotTo(ContainSubstring("scope="))
@@ -202,4 +215,78 @@ var _ = Describe("ADFS Provider Tests", func() {
202215 }),
203216 )
204217 })
218+
219+ Context("UPN Fallback", func() {
220+ var idToken string
221+ var session *sessions.SessionState
222+
223+ BeforeEach(func() {
224+ var err error
225+ idToken, err = newSignedTestADFSToken(adfsClaims{
226+ UPN: "upn@company.com",
227+ idTokenClaims: minimalIDToken,
228+ })
229+ Expect(err).ToNot(HaveOccurred())
230+
231+ session = &sessions.SessionState{
232+ IDToken: idToken,
233+ }
234+ })
235+
236+ Describe("EnrichSession", func() {
237+ It("uses email claim if present", func() {
238+ p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
239+ s.Email = "person@company.com"
240+ return nil
241+ }
242+
243+ err := p.EnrichSession(context.Background(), session)
244+ Expect(err).ToNot(HaveOccurred())
245+ Expect(session.Email).To(Equal("person@company.com"))
246+ })
247+
248+ It("falls back to UPN claim if Email is missing", func() {
249+ p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
250+ return nil
251+ }
252+
253+ err := p.EnrichSession(context.Background(), session)
254+ Expect(err).ToNot(HaveOccurred())
255+ Expect(session.Email).To(Equal("upn@company.com"))
256+ })
257+
258+ It("falls back to UPN claim on errors", func() {
259+ p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
260+ return errors.New("neither the id_token nor the profileURL set an email")
261+ }
262+
263+ err := p.EnrichSession(context.Background(), session)
264+ Expect(err).ToNot(HaveOccurred())
265+ Expect(session.Email).To(Equal("upn@company.com"))
266+ })
267+ })
268+
269+ Describe("RefreshSession", func() {
270+ It("uses email claim if present", func() {
271+ p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
272+ s.Email = "person@company.com"
273+ return true, nil
274+ }
275+
276+ _, err := p.RefreshSession(context.Background(), session)
277+ Expect(err).ToNot(HaveOccurred())
278+ Expect(session.Email).To(Equal("person@company.com"))
279+ })
280+
281+ It("falls back to UPN claim if Email is missing", func() {
282+ p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
283+ return true, nil
284+ }
285+
286+ _, err := p.RefreshSession(context.Background(), session)
287+ Expect(err).ToNot(HaveOccurred())
288+ Expect(session.Email).To(Equal("upn@company.com"))
289+ })
290+ })
291+ })
205292})