git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: ability to parse JWT encoded profile claims (#3014)
b7ff804ikarius
CHANGELOG.md+1
@@ -12,6 +12,7 @@
12- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
12- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
13- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
13- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
14- [#1916](https://github.com/oauth2-proxy/oauth2-proxy/pull/1916) fix: role extraction from access token in keycloak oidc (@Elektordi / @tuunit)
14- [#1916](https://github.com/oauth2-proxy/oauth2-proxy/pull/1916) fix: role extraction from access token in keycloak oidc (@Elektordi / @tuunit)
15+- [#3014](https://github.com/oauth2-proxy/oauth2-proxy/pull/3014) feat: ability to parse JWT encoded profile claims (@ikarius)
15
16
16# V7.8.2
17# V7.8.2
17
18
pkg/providers/util/claim_extractor.go+18−3
@@ -5,6 +5,7 @@ import (
5 "encoding/base64"
5 "encoding/base64"
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8+ "mime"
8 "net/http"
9 "net/http"
9 "net/url"
10 "net/url"
10 "strings"
11 "strings"
@@ -94,11 +95,25 @@ func (c *claimExtractor) loadProfileClaims() (*simplejson.Json, error) {
94 return simplejson.New(), nil
95 return simplejson.New(), nil
95 }
96 }
96
97
97− claims, err := requests.New(c.profileURL.String()).
98+ builder := requests.New(c.profileURL.String()).
98 WithContext(c.ctx).
99 WithContext(c.ctx).
99 WithHeaders(c.requestHeaders).
100 WithHeaders(c.requestHeaders).
100− Do().
101+ Do()
102+
103+ // We first check if the result is a JWT token
104+ // https://openid.net/specs/openid-connect-core-1_0-final.html#UserInfoResponse
105+ mediaType, _, parseErr := mime.ParseMediaType(builder.Headers().Get("Content-Type"))
106+
107+ if parseErr == nil && mediaType == "application/jwt" {
108+ // Decode and use JWT payload as profile claims
109+ if pl, err := parseJWT(string(builder.Body())); err == nil {
110+ return simplejson.NewJson(pl)
111+ }
112+ }
113+
114+ // Otherwise, process as normal JSON payload
101− UnmarshalSimpleJSON()
115+ claims, err := builder.UnmarshalSimpleJSON()
116+
102 if err != nil {
117 if err != nil {
103 return nil, fmt.Errorf("error making request to profile URL: %v", err)
118 return nil, fmt.Errorf("error making request to profile URL: %v", err)
104 }
119 }
pkg/providers/util/claim_extractor_test.go+48
@@ -497,6 +497,54 @@ var _ = Describe("Claim Extractor Suite", func() {
497 expectedDst: stringPointer("{\"foo\":[\"bar\",\"baz\"]}"),
497 expectedDst: stringPointer("{\"foo\":[\"bar\",\"baz\"]}"),
498 }),
498 }),
499 )
499 )
500+
501+ It("should extract claims from a JWT response", func() {
502+ jwtResponsePayload := `{
503+ "user": "jwtUser",
504+ "email": "jwtEmail",
505+ "groups": [
506+ "jwtGroup1",
507+ "jwtGroup2"
508+ ]
509+ }`
510+
511+ jwtResponseHandler := func(rw http.ResponseWriter, req *http.Request) {
512+ if !hasAuthorizedHeader(req.Header) {
513+ rw.WriteHeader(403)
514+ rw.Write([]byte("Unauthorized"))
515+ return
516+ }
517+
518+ rw.Header().Set("Content-Type", "application/jwt; charset=utf-8")
519+ rw.Write([]byte(createJWTFromPayload(jwtResponsePayload)))
520+ }
521+
522+ claimExtractor, serverClose, err := newTestClaimExtractor(testClaimExtractorOpts{
523+ idTokenPayload: emptyJSON,
524+ setProfileURL: true,
525+ profileRequestHeaders: newAuthorizedHeader(),
526+ profileRequestHandler: jwtResponseHandler,
527+ })
528+ Expect(err).ToNot(HaveOccurred())
529+ if serverClose != nil {
530+ defer serverClose()
531+ }
532+
533+ value, exists, err := claimExtractor.GetClaim("user")
534+ Expect(err).ToNot(HaveOccurred())
535+ Expect(exists).To(BeTrue())
536+ Expect(value).To(Equal("jwtUser"))
537+
538+ value, exists, err = claimExtractor.GetClaim("email")
539+ Expect(err).ToNot(HaveOccurred())
540+ Expect(exists).To(BeTrue())
541+ Expect(value).To(Equal("jwtEmail"))
542+
543+ value, exists, err = claimExtractor.GetClaim("groups")
544+ Expect(err).ToNot(HaveOccurred())
545+ Expect(exists).To(BeTrue())
546+ Expect(value).To(Equal([]interface{}{"jwtGroup1", "jwtGroup2"}))
547+ })
500})
548})
501
549
502// ******************************************
550// ******************************************