git.iko.soy/typeable/oauth2-proxy
on master help sign in
Use upn claim as a fallback in Enrich & Refresh

Only when `email` claim is missing, fallback to `upn` claim which may have it.

commit4980f6af7d86337aa66cfb3dfbebccb0d9df8b80 authorNick Meves <nick.meves@greenhouse.io> dateTue 22 Jun 2021, 18:50 (5 years ago) parenta531987
2 files changed+138−10
providers/adfs.go+59−7 providers/adfs_test.go+79−3
providers/adfs.go+59−7
@@ -1,14 +1,22 @@
11package providers
22
33import (
4+ "context"
5+ "fmt"
46 "net/url"
57 "strings"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
610)
711
812// ADFSProvider represents an ADFS based Identity Provider
913type ADFSProvider struct {
1014 *OIDCProvider
15+
1116 skipScope bool
17+ // Expose for unit testing
18+ oidcEnrichFunc func(context.Context, *sessions.SessionState) error
19+ oidcRefreshFunc func(context.Context, *sessions.SessionState) (bool, error)
1220}
1321
1422var _ Provider = (*ADFSProvider)(nil)
@@ -17,7 +25,7 @@ const (
1725 adfsProviderName = "ADFS"
1826 adfsDefaultScope = "openid email profile"
1927 adfsSkipScope = false
20− adfsEmailClaim = "upn"
28+ adfsUPNClaim = "upn"
2129)
2230
2331// NewADFSProvider initiates a new ADFSProvider
@@ -26,7 +34,6 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
2634 name: adfsProviderName,
2735 scope: adfsDefaultScope,
2836 })
29− p.EmailClaim = adfsEmailClaim
3037
3138 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
3239 resource := p.ProtectedResource.String()
@@ -39,12 +46,16 @@ func NewADFSProvider(p *ProviderData) *ADFSProvider {
3946 }
4047 }
4148
49+ oidcProvider := &OIDCProvider{
50+ ProviderData: p,
51+ SkipNonce: true,
52+ }
53+
4254 return &ADFSProvider{
43− OIDCProvider: &OIDCProvider{
44− ProviderData: p,
45− SkipNonce: true,
46− },
47− skipScope: adfsSkipScope,
55+ OIDCProvider: oidcProvider,
56+ skipScope: adfsSkipScope,
57+ oidcEnrichFunc: oidcProvider.EnrichSession,
58+ oidcRefreshFunc: oidcProvider.RefreshSession,
4859 }
4960}
5061
@@ -68,3 +79,44 @@ func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
6879 }
6980 return loginURL.String()
7081}
82+
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.
85+func (p *ADFSProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
86+ err := p.oidcEnrichFunc(ctx, s)
87+ if err != nil {
88+ return err
89+ }
90+
91+ if s.Email == "" {
92+ return p.fallbackUPN(ctx, s)
93+ }
94+ return nil
95+}
96+
97+// RefreshSession refreshes via the OIDC implementation. If email is missing,
98+// falls back to ADFS `upn` claim.
99+func (p *ADFSProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
100+ refreshed, err := p.oidcRefreshFunc(ctx, s)
101+ if err != nil || s.Email != "" {
102+ return refreshed, err
103+ }
104+ err = p.fallbackUPN(ctx, s)
105+ return refreshed, err
106+}
107+
108+func (p *ADFSProvider) fallbackUPN(ctx context.Context, s *sessions.SessionState) error {
109+ idToken, err := p.Verifier.Verify(ctx, s.IDToken)
110+ if err != nil {
111+ return err
112+ }
113+ claims, err := p.getClaims(idToken)
114+ if err != nil {
115+ return fmt.Errorf("couldn't extract claims from id_token (%v)", err)
116+ }
117+ upn := claims.raw[adfsUPNClaim]
118+ if upn != nil {
119+ s.Email = fmt.Sprint(upn)
120+ }
121+ return nil
122+}
providers/adfs_test.go+79−3
@@ -2,6 +2,8 @@ package providers
22
33import (
44 "context"
5+ "crypto/rand"
6+ "crypto/rsa"
57 "encoding/base64"
68 "net/http"
79 "net/http/httptest"
@@ -9,6 +11,7 @@ import (
911 "strings"
1012
1113 "github.com/coreos/go-oidc/v3/oidc"
14+ "github.com/dgrijalva/jwt-go"
1215 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1316 . "github.com/onsi/ginkgo"
1417 . "github.com/onsi/ginkgo/extensions/table"
@@ -25,8 +28,18 @@ func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []by
2528 return decodeString, nil
2629}
2730
28−func testADFSProvider(hostname string) *ADFSProvider {
31+type adfsClaims struct {
32+ UPN string `json:"upn,omitempty"`
33+ idTokenClaims
34+}
35+
36+func newSignedTestADFSToken(tokenClaims adfsClaims) (string, error) {
37+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
38+ standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
39+ return standardClaims.SignedString(key)
40+}
2941
42+func testADFSProvider(hostname string) *ADFSProvider {
3043 o := oidc.NewVerifier(
3144 "https://issuer.example.com",
3245 fakeADFSJwks{},
@@ -41,6 +54,7 @@ func testADFSProvider(hostname string) *ADFSProvider {
4154 ValidateURL: &url.URL{},
4255 Scope: "",
4356 Verifier: o,
57+ EmailClaim: OIDCEmailClaim,
4458 })
4559
4660 if hostname != "" {
@@ -54,7 +68,6 @@ func testADFSProvider(hostname string) *ADFSProvider {
5468}
5569
5670func testADFSBackend() *httptest.Server {
57−
5871 authResponse := `
5972 {
6073 "access_token": "my_access_token",
@@ -129,7 +142,6 @@ var _ = Describe("ADFS Provider Tests", func() {
129142
130143 Context("with valid token", func() {
131144 It("should not throw an error", func() {
132− p.EmailClaim = "email"
133145 rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
134146 idToken, err := p.Verifier.Verify(context.Background(), rawIDToken)
135147 Expect(err).To(BeNil())
@@ -202,4 +214,68 @@ var _ = Describe("ADFS Provider Tests", func() {
202214 }),
203215 )
204216 })
217+
218+ Context("UPN Fallback", func() {
219+ var idToken string
220+ var session *sessions.SessionState
221+
222+ BeforeEach(func() {
223+ var err error
224+ idToken, err = newSignedTestADFSToken(adfsClaims{
225+ UPN: "upn@company.com",
226+ idTokenClaims: minimalIDToken,
227+ })
228+ Expect(err).ToNot(HaveOccurred())
229+
230+ session = &sessions.SessionState{
231+ IDToken: idToken,
232+ }
233+ })
234+
235+ Describe("EnrichSession", func() {
236+ It("uses email claim if present", func() {
237+ p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
238+ s.Email = "person@company.com"
239+ return nil
240+ }
241+
242+ err := p.EnrichSession(context.Background(), session)
243+ Expect(err).ToNot(HaveOccurred())
244+ Expect(session.Email).To(Equal("person@company.com"))
245+ })
246+
247+ It("falls back to UPN claim if Email is missing", func() {
248+ p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
249+ return nil
250+ }
251+
252+ err := p.EnrichSession(context.Background(), session)
253+ Expect(err).ToNot(HaveOccurred())
254+ Expect(session.Email).To(Equal("upn@company.com"))
255+ })
256+ })
257+
258+ Describe("RefreshSession", func() {
259+ It("uses email claim if present", func() {
260+ p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
261+ s.Email = "person@company.com"
262+ return true, nil
263+ }
264+
265+ _, err := p.RefreshSession(context.Background(), session)
266+ Expect(err).ToNot(HaveOccurred())
267+ Expect(session.Email).To(Equal("person@company.com"))
268+ })
269+
270+ It("falls back to UPN claim if Email is missing", func() {
271+ p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
272+ return true, nil
273+ }
274+
275+ _, err := p.RefreshSession(context.Background(), session)
276+ Expect(err).ToNot(HaveOccurred())
277+ Expect(session.Email).To(Equal("upn@company.com"))
278+ })
279+ })
280+ })
205281})